diff --git a/.github/workflows/build_packages.yml b/.github/workflows/build_packages.yml
new file mode 100644
index 000000000..a89658e2f
--- /dev/null
+++ b/.github/workflows/build_packages.yml
@@ -0,0 +1,29 @@
+name: Build all packages
+
+on: [push, pull_request]
+
+env:
+  PACKAGE_INDEX_PATH: /tmp/micropython-lib-deploy
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - uses: actions/setup-python@v4
+    - name: Setup environment
+      run: source tools/ci.sh && ci_build_packages_setup
+    - name: Check manifest files
+      run: source tools/ci.sh && ci_build_packages_check_manifest
+    - name: Compile package index
+      run: source tools/ci.sh && ci_build_packages_compile_index
+    - name: Compile package examples
+      run: source tools/ci.sh && ci_build_packages_examples
+    - name: Publish packages for branch
+      if: vars.MICROPY_PUBLISH_MIP_INDEX && github.event_name == 'push' && ! github.event.deleted
+      run: source tools/ci.sh && ci_push_package_index
+    - name: Upload packages as artifact
+      uses: actions/upload-artifact@v4
+      with:
+        name: packages-${{ github.sha }}
+        path: ${{ env.PACKAGE_INDEX_PATH }}
diff --git a/.github/workflows/cleanup_published_packages.yml b/.github/workflows/cleanup_published_packages.yml
new file mode 100644
index 000000000..040b09ff4
--- /dev/null
+++ b/.github/workflows/cleanup_published_packages.yml
@@ -0,0 +1,12 @@
+name: Cleanup published packages
+
+on: delete
+
+jobs:
+  cleanup:
+    runs-on: ubuntu-latest
+    if: vars.MICROPY_PUBLISH_MIP_INDEX
+    steps:
+    - uses: actions/checkout@v3
+    - name: Clean up published files
+      run: source tools/ci.sh && ci_cleanup_package_index ${{ github.event.ref }}
diff --git a/.github/workflows/commit_formatting.yml b/.github/workflows/commit_formatting.yml
new file mode 100644
index 000000000..a651f8a13
--- /dev/null
+++ b/.github/workflows/commit_formatting.yml
@@ -0,0 +1,18 @@
+name: Check commit message formatting
+
+on: [push, pull_request]
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        fetch-depth: '100'
+    - uses: actions/setup-python@v4
+    - name: Check commit message formatting
+      run: source tools/ci.sh && ci_commit_formatting_run
diff --git a/.github/workflows/package_tests.yml b/.github/workflows/package_tests.yml
new file mode 100644
index 000000000..5e503509e
--- /dev/null
+++ b/.github/workflows/package_tests.yml
@@ -0,0 +1,16 @@
+name: Package tests
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - uses: actions/setup-python@v4
+    - name: Setup environment
+      run: source tools/ci.sh && ci_package_tests_setup_micropython
+    - name: Setup libraries
+      run: source tools/ci.sh && ci_package_tests_setup_lib
+    - name: Run tests
+      run: source tools/ci.sh && ci_package_tests_run
diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml
new file mode 100644
index 000000000..b347e34ee
--- /dev/null
+++ b/.github/workflows/ruff.yml
@@ -0,0 +1,12 @@
+# https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
+name: Python code lint and formatting with ruff
+on: [push, pull_request]
+jobs:
+  ruff:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    # Version should be kept in sync with .pre-commit_config.yaml & also micropython
+    - run: pip install --user ruff==0.11.6
+    - run: ruff check --output-format=github .
+    - run: ruff format --diff .
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..05f5d3df0
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,15 @@
+repos:
+  - repo: local
+    hooks:
+      - id: verifygitlog
+        name: MicroPython git commit message format checker
+        entry: tools/verifygitlog.py --check-file --ignore-rebase
+        language: python
+        verbose: true
+        stages: [commit-msg]
+  - repo: https://github.com/charliermarsh/ruff-pre-commit
+    # Version should be kept in sync with .github/workflows/ruff.yml & also micropython
+    rev: v0.11.6
+    hooks:
+      - id: ruff
+        id: ruff-format
diff --git a/CODEOFCONDUCT.md b/CODEOFCONDUCT.md
new file mode 100644
index 000000000..1401e5f5e
--- /dev/null
+++ b/CODEOFCONDUCT.md
@@ -0,0 +1 @@
+Please see the [MicroPython Code of Conduct](https://github.com/micropython/micropython/blob/master/CODEOFCONDUCT.md).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 919a69967..61a49101e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,3 +1,132 @@
-If you submit a pull request, please adhere to Contributor Guidelines:
+## Contributor's Guidelines & Code Conventions
 
-https://github.com/micropython/micropython-lib/wiki/ContributorGuidelines
+micropython-lib follows the same general conventions as the [main MicroPython
+repository](https://github.com/micropython/micropython). Please see
+[micropython/CONTRIBUTING.md](https://github.com/micropython/micropython/blob/master/CONTRIBUTING.md)
+and [micropython/CODECONVENTIONS.md](https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md).
+
+### Raising issues
+
+Please include enough information for someone to reproduce the issue you are
+describing. This will typically include:
+
+* The version of MicroPython you are using (e.g. the firmware filename, git
+  hash, or version info printed by the startup message).
+* What board/device you are running MicroPython on.
+* Which package you have installed, how you installed it, and what version.
+  When installed via `mip`, all packages will have a `__version__`
+  attribute.
+* A simple code snippet that demonstrates the issue.
+
+If you have a how-to question or are looking for help with using MicroPython
+or packages from micropython-lib, please post at the
+[discussion forum](https://github.com/orgs/micropython/discussions) instead.
+
+### Pull requests
+
+The same rules for commit messages, signing-off commits, and commit structure
+apply [as for the main MicroPython repository](https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md).
+
+All Python code is formatted using the [black](https://github.com/psf/black)
+tool. You can run [`tools/codeformat.py`](tools/codeformat.py) to apply
+`black` automatically before submitting a PR. The GitHub CI will also run the
+[ruff](https://github.com/astral-sh/ruff) tool to apply further "linting"
+checks.
+
+Similar to the main repository, a configuration is provided for the
+[pre-commit](https://pre-commit.com/) tool to apply `black` code formatting
+rules and run `ruff` automatically. See the documentation for using pre-commit
+in [the code conventions document](https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md#automatic-pre-commit-hooks)
+
+In addition to the conventions from the main repository, there are some
+specific conventions and guidelines for micropython-lib:
+
+* The first line of the commit message should start with the name of the
+  package, followed by a short description of the commit. Package names are
+  globally unique in the micropython-lib directory structure.
+
+  For example: `shutil: Add disk_usage function.`
+
+* Although we encourage keeping the code short and minimal, please still use
+  comments in your code. Typically, packages will be installed via
+  `mip` and so they will be compiled to bytecode where comments will
+  _not_ contribute to the installed size.
+
+* All packages must include a `manifest.py`, including a `metadata()` line
+  with at least a description and a version.
+
+* Prefer to break larger packages up into smaller chunks, so that just the
+  required functionality can be installed. The way to do this is to have a
+  base package, e.g. `mypackage` containing `mypackage/__init__.py`, and then
+  an "extension" package, e.g. `mypackage-ext` containing additional files
+  e.g. `mypackage/ext.py`. See
+  [`collections-defaultdict`](python-stdlib/collections-defaultdict) as an
+  example.
+
+* If you think a package might be extended in this way in the future, prefer
+  to create a package directory with `package/__init__.py`, rather than a
+  single `module.py`.
+
+* Packages in the python-stdlib directory should be CPython compatible and
+  implement a subset of the CPython equivalent. Avoid adding
+  MicroPython-specific extensions. Please include a link to the corresponding
+  CPython docs in the PR.
+
+* Include tests (ideally using the `unittest` package) as `test_*.py`.
+  Otherwise, provide examples as `example_*.py`. When porting CPython
+  packages, prefer to use the existing tests rather than writing new ones
+  from scratch.
+
+* When porting an existing third-party package, please ensure that the source
+  license is compatible.
+
+* To make it easier for others to install packages directly from your PR before
+  it is merged, consider opting-in to automatic package publishing (see
+  [Publishing packages from forks](#publishing-packages-from-forks)). If you do
+  this, consider quoting the [commands to install
+  packages](README.md#installing-packages-from-forks) in your Pull Request
+  description.
+
+### Publishing packages from forks
+
+You can easily publish the packages from your micropython-lib
+[fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks)
+by opting in to a system based on [GitHub
+Actions](https://docs.github.com/en/actions) and [GitHub
+Pages](https://docs.github.com/en/pages):
+
+1. Open your fork's repository in the GitHub web interface.
+2. Navigate to "Settings" -> "Secrets and variables" -> "Actions" -> "Variables".
+3. Click "New repository variable"
+4. Create a variable named `MICROPY_PUBLISH_MIP_INDEX` with value `true` (or any
+   "truthy" value).
+5. The settings for GitHub Actions and GitHub Pages features should not need to
+   be changed from the repository defaults, unless you've explicitly disabled
+   Actions or Pages in your fork.
+
+The next time you push commits to a branch in your fork, GitHub Actions will run
+an additional step in the "Build All Packages" workflow named "Publish Packages
+for branch". This step runs in *your fork*, but if you open a pull request then
+this workflow is not shown in the Pull Request's "Checks". These run in the
+upstream repository. Navigate to your fork's Actions tab in order to see
+the additional "Publish Packages for branch" step.
+
+Anyone can then install these packages as described under [Installing packages
+from forks](README.md#installing-packages-from-forks).
+
+The exact command is also quoted in the GitHub Actions log in your fork's
+Actions for the "Publish Packages for branch" step of "Build All Packages".
+
+#### Opting Back Out
+
+To opt-out again, delete the `MICROPY_PUBLISH_MIP_INDEX` variable and
+(optionally) delete the `gh-pages` branch from your fork.
+
+*Note*: While enabled, all micropython-lib packages will be published each time
+a change is pushed to any branch in your fork. A commit is added to the
+`gh-pages` branch each time. In a busy repository, the `gh-pages` branch may
+become quite large. The actual `.git` directory size on disk should still be
+quite small, as most of the content will be duplicated. If you're worried that
+the `gh-pages` branch has become too large then you can always delete this
+branch from GitHub. GitHub Actions will create a new `gh-pages` branch the next
+time you push a change.
diff --git a/LICENSE b/LICENSE
index 87095e296..0e9e41d35 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,8 +1,8 @@
 micropython-lib consists of multiple modules from different sources and
-authors. Each module comes under its own licensing terms. Short name of
-a license can be found in a file within a module directory (usually
-metadata.txt or setup.py). Complete text of each license used is provided
-below. Files not belonging to a particular module a provided under MIT
+authors. Each module comes under its own licensing terms. The short name of
+a license can be found in a file within the module directory (usually
+metadata.txt or setup.py). The complete text of each license used is provided
+below. Files not belonging to a particular module are provided under the MIT
 license, unless explicitly stated otherwise.
 
 =============== MIT License ===============
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 7ae088359..000000000
--- a/Makefile
+++ /dev/null
@@ -1,16 +0,0 @@
-PREFIX = ~/.micropython/lib
-
-all:
-
-# Installs all modules to a lib location, for development testing
-CMD="find . -maxdepth 1 -mindepth 1 \( -name '*.py' -not -name 'test_*' -not -name 'setup.py' \) -or \( -type d -not -name 'dist' -not -name '*.egg-info' -not -name '__pycache__' \)| xargs --no-run-if-empty cp -r -t $(PREFIX)"
-install:
-	@mkdir -p $(PREFIX)
-	@if [ -n "$(MOD)" ]; then \
-	    (cd $(MOD); sh -c $(CMD)); \
-	else \
-	    for d in $$(find -maxdepth 1 -type d ! -name ".*"); do \
-	        echo $$d; \
-	        (cd $$d; sh -c $(CMD)); \
-	    done \
-	fi
diff --git a/README.md b/README.md
index 2f893f256..73417b965 100644
--- a/README.md
+++ b/README.md
@@ -1,66 +1,172 @@
-~~~~
-!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-micropython-lib is a highly experimental community project.
+# micropython-lib
 
-Please help to drive it to just "experimental" state by testing
-provided packages with MicroPython.
-!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-~~~~
+This is a repository of packages designed to be useful for writing MicroPython
+applications.
 
-micropython-lib
-===============
-micropython-lib is a project to develop a non-monolothic standard library
-for MicroPython. Each module or package is available as a separate
-distribution package from PyPI. Each module is either written from scratch or
-ported from CPython. 
+The packages here fall into categories corresponding to the four top-level
+directories:
 
-Note that the main target of micropython-lib is a "Unix" port of MicroPython
-(additional ports to support are to be determined). Actual system requirements
-vary per module. Though if a module is not related to I/O, the module should
-work without problem on bare-metal ports too (e.g. pyboard).
+* **python-stdlib**: Compatible versions of modules from [The Python Standard
+    Library](https://docs.python.org/3/library/). These should be drop-in
+    replacements for the corresponding Python modules, although many have
+    reduced functionality or missing methods or classes (which may not be an
+    issue for most cases).
 
+ * **python-ecosys**: Compatible, but reduced-functionality versions of
+     packages from the wider Python ecosystem. For example, a package that
+     might be found in the [Python Package Index](https://pypi.org/).
 
-Usage
------
-micropython-lib packages are published on PyPI (Python Package Index),
-the standard Python community package repository: http://pypi.python.org/ .
-On PyPi, you can search for MicroPython related packages and read
-additional package information.
+ * **micropython**: MicroPython-specific packages that do not have equivalents
+     in other Python environments. This includes drivers for hardware
+     (e.g. sensors, peripherals, or displays), libraries to work with
+     embedded functionality (e.g. bluetooth), or MicroPython-specific
+     packages that do not have equivalents in CPython.
 
-To install packages from PyPI for usage on your local system, use the
-`pip-micropython` tool, which is a simple wrapper around the standard
-`pip` tool, which is used to install packages for CPython.
-The `pip-micropython` tool can be found in `tools` subdirectory 
-of the main MicroPython repository (https://github.com/micropython/micropython).
-Just install the `pip-micropython` script somewhere on your `PATH`.
+* **unix-ffi**: These packages are specifically for the MicroPython Unix port
+    and provide access to operating-system and third-party libraries via FFI,
+    or functionality that is not useful for non-Unix ports.
 
-Afterwards, just use `pip-micropython` in a way similar to `pip`:
+## Usage
 
-~~~~
-$ pip-micropython install micropython-copy
-$ micropython
->>> import copy
->>> copy.copy([1, 2, 3])
-[1, 2, 3]
-~~~~
+To install a micropython-lib package, there are four main options. For more
+information see the [Package management documentation](https://docs.micropython.org/en/latest/reference/packages.html)
+documentation.
 
-Review the `pip-micropython` source code for more info.
+### On a network-enabled device
 
+As of MicroPython v1.20 (and nightly builds since October 2022), boards
+with WiFi and Ethernet support include the `mip` package manager.
 
-Development
------------
-To install modules during development, use `make install`. By default, all
-available packages will be installed. To install a specific module, add the 
-`MOD=<module>` parameter to the end of the `make install` command.
+```py
+>>> import mip
+>>> mip.install("package-name")
+```
 
+### Using `mpremote` from your PC
 
-Links
------
-More information is on GitHub and in the MicroPython forums:
+`mpremote` is the officially-supported tool for interacting with a MicroPython
+device and, since v0.4.0, support for installing micropython-lib packages is
+provided by using the `mip` command.
 
- * https://github.com/micropython/micropython/issues/405
- * http://forum.micropython.org/viewtopic.php?f=5&t=70
+```bash
+$ mpremote connect /dev/ttyUSB0 mip install package-name
+```
 
-Guidelines for packaging MicroPython modules for PyPI:
+See the [mpremote documentation](https://docs.micropython.org/en/latest/reference/mpremote.html).
+
+### Freeze into your firmware
+
+If you are building your own firmware, all packages in this repository include
+a `manifest.py` that can be included into your board manifest via the
+`require()` command. See [Manifest files](https://docs.micropython.org/en/latest/reference/manifest.html#require) for
+more information.
+
+### Copy the files manually
+
+Many micropython-lib packages are just single-file modules, and you can
+quickly get started by copying the relevant Python file to your device. For
+example, to add the `base64` library, you can directly copy
+`python-stdlib/base64/base64.py` to the `lib` directory on your device.
+
+This can be done using `mpremote`, for example:
+
+```bash
+$ mpremote connect /dev/ttyUSB0 cp python-stdlib/base64/base64.py :/lib
+```
+
+For packages that are implemented as a package directory, you'll need to copy
+the directory instead. For example, to add `collections.defaultdict`, copy
+`collections/collections/__init__.py` and
+`collections-defaultdict/collections/defaultdict.py` to a directory named
+`lib/collections` on your device.
+
+Note that unlike the other three approaches based on `mip` or `manifest.py`,
+you will need to manually resolve dependencies. You can inspect the relevant
+`manifest.py` file to view the list of dependencies for a given package.
+
+## Installing packages from forks
+
+It is possible to use the `mpremote mip install` or `mip.install()` methods to
+install packages built from a
+[fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks)
+of micropython-lib, if the fork's owner has opted in.
+
+This can be useful to install packages from a pending Pull Request, for example.
+
+First, the owner of the fork must opt-in as described under
+[Publishing packages from forks](CONTRIBUTING.md#publishing-packages-from-forks).
+
+After this has happened, each time someone pushes to a branch in that fork then
+GitHub Actions will automatically publish the packages to a GitHub Pages site.
+
+To install these packages, use commands such as:
+
+```bash
+$ mpremote connect /dev/ttyUSB0 mip install --index https://USERNAME.github.io/micropython-lib/mip/BRANCH_NAME PACKAGE_NAME
+```
+
+Or from a networked device:
+
+```py
+import mip
+mip.install(PACKAGE_NAME, index="https://USERNAME.github.io/micropython-lib/mip/BRANCH_NAME")
+```
+
+(Where `USERNAME`, `BRANCH_NAME` and `PACKAGE_NAME` are replaced with the owner
+of the fork, the branch the packages were built from, and the package name.)
+
+## Contributing
+
+We use [GitHub Discussions](https://github.com/micropython/micropython/discussions)
+as our forum, and [Discord](https://micropython.org/discord) for chat. These
+are great places to ask questions and advice from the community or to discuss your
+MicroPython-based projects.
+
+The [MicroPython Wiki](https://github.com/micropython/micropython/wiki) is
+also used for micropython-lib.
+
+For bugs and feature requests, please [raise an issue](https://github.com/micropython/micropython-lib/issues/new).
+
+We welcome pull requests to add new packages, fix bugs, or add features.
+Please be sure to follow the
+[Contributor's Guidelines & Code Conventions](CONTRIBUTING.md). Note that
+MicroPython is licensed under the [MIT license](LICENSE) and all contributions
+should follow this license.
+
+### Future plans (and new contributor ideas)
+
+* Develop a set of example programs using these packages.
+* Develop more MicroPython packages for common tasks.
+* Expand unit testing coverage.
+* Add support for referencing remote/third-party repositories.
+
+## Notes on terminology
+
+The terms *library*, *package*, and *module* are overloaded and lead to some
+confusion. The interpretation used in by the MicroPython project is that:
+
+A *library* is a collection of installable packages, e.g. [The Python Standard
+  Library](https://docs.python.org/3/library/), or micropython-lib.
+
+A *package* can refer to two things. The first meaning, "library package", is
+something that can be installed from a library, e.g. via `mip` (or `pip` in
+CPython/PyPI). Packages provide *modules* that can be imported. The ambiguity
+here is that the module provided by the package does not necessarily have to
+have the same name, e.g. the `pyjwt` package provides the `jwt` module. In
+CPython, the `pyserial` package providing the `serial` module is another
+common example.
+
+A *module* is something that can be imported. For example, "the *os* module".
+
+A module can be implemented either as a single file, typically also called
+a *module* or "single-file module", or as a *package* (the second meaning),
+which in this context means a directory containing multiple `.py` files
+(usually at least an `__init__.py`).
+
+In micropython-lib, we also have the concept of an *extension package* which
+is a library package that extends the functionality of another package, by
+adding additional files to the same package directory. These packages have
+hyphenated names. For example, the `collections-defaultdict` package extends
+the `collections` package to add the `defaultdict` class to the `collections`
+module.
 
- * https://github.com/micropython/micropython/issues/413
diff --git a/__future__/metadata.txt b/__future__/metadata.txt
deleted file mode 100644
index 611cf1ae5..000000000
--- a/__future__/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.2
-dist_name=future
diff --git a/__future__/setup.py b/__future__/setup.py
deleted file mode 100644
index 23d34acb3..000000000
--- a/__future__/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-future',
-      version='0.0.2',
-      description='Dummy __future__ module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['__future__'])
diff --git a/_libc/metadata.txt b/_libc/metadata.txt
deleted file mode 100644
index 14e65a6d9..000000000
--- a/_libc/metadata.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-dist_name = libc
-srctype = micropython-lib
-type = module
-version = 0.3
-author = Paul Sokolovsky
-desc = MicroPython FFI helper module (deprecated)
-long_desc = MicroPython FFI helper module (deprecated, replaced by micropython-ffilib).
diff --git a/_libc/setup.py b/_libc/setup.py
deleted file mode 100644
index 659622ebb..000000000
--- a/_libc/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-libc',
-      version='0.3',
-      description='MicroPython FFI helper module (deprecated)',
-      long_description='MicroPython FFI helper module (deprecated, replaced by micropython-ffilib).',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['_libc'])
diff --git a/_markupbase/metadata.txt b/_markupbase/metadata.txt
deleted file mode 100644
index 6fca6d87a..000000000
--- a/_markupbase/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = module
-version = 3.3.3
-depends = re-pcre
diff --git a/_markupbase/setup.py b/_markupbase/setup.py
deleted file mode 100644
index aaa0b3ae3..000000000
--- a/_markupbase/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-_markupbase',
-      version='3.3.3',
-      description='CPython _markupbase module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['_markupbase'],
-      install_requires=['micropython-re-pcre'])
diff --git a/abc/abc.py b/abc/abc.py
deleted file mode 100644
index 941be4f5e..000000000
--- a/abc/abc.py
+++ /dev/null
@@ -1,2 +0,0 @@
-def abstractmethod(f):
-    return f
diff --git a/abc/metadata.txt b/abc/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/abc/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/abc/setup.py b/abc/setup.py
deleted file mode 100644
index 11b68b630..000000000
--- a/abc/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-abc',
-      version='0.0.0',
-      description='Dummy abc module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['abc'])
diff --git a/argparse/metadata.txt b/argparse/metadata.txt
deleted file mode 100644
index c7f358133..000000000
--- a/argparse/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.3.1
-author = Damien George
diff --git a/argparse/setup.py b/argparse/setup.py
deleted file mode 100644
index 5b3dc4bc9..000000000
--- a/argparse/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-argparse',
-      version='0.3.1',
-      description='argparse module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Damien George',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['argparse'])
diff --git a/argparse/test_argparse.py b/argparse/test_argparse.py
deleted file mode 100644
index 7c5651c95..000000000
--- a/argparse/test_argparse.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import argparse
-
-parser = argparse.ArgumentParser(description="command line program")
-parser.add_argument("a")
-parser.add_argument("b")
-parser.add_argument(dest="c")
-args = parser.parse_args(["1", "2", "3"])
-assert args.a == "1" and args.b == "2" and args.c == "3"
-
-parser = argparse.ArgumentParser()
-parser.add_argument("-a", action="store_true")
-parser.add_argument("-b", default=123)
-args = parser.parse_args([])
-assert args.a is False and args.b == 123
-args = parser.parse_args(["-a"])
-assert args.a is True and args.b == 123
-args = parser.parse_args(["-b", "456"])
-assert args.a is False and args.b == "456"
-
-parser = argparse.ArgumentParser()
-parser.add_argument("-a", "--a-opt", action="store_true")
-parser.add_argument("-b", "--b-opt", default=123)
-parser.add_argument("--c-opt", default="test")
-args = parser.parse_args([])
-assert args.a_opt is False and args.b_opt == 123 and args.c_opt == "test"
-args = parser.parse_args(["--a-opt"])
-assert args.a_opt is True and args.b_opt == 123 and args.c_opt == "test"
-args = parser.parse_args(["--b-opt", "456"])
-assert args.a_opt is False and args.b_opt == "456" and args.c_opt == "test"
-args = parser.parse_args(["--c-opt", "override"])
-assert args.a_opt is False and args.b_opt == 123 and args.c_opt == "override"
-
-parser = argparse.ArgumentParser()
-parser.add_argument("files", nargs="+")
-args = parser.parse_args(["a"])
-assert args.files == ["a"]
-args = parser.parse_args(["a", "b", "c"])
-assert args.files == ["a", "b", "c"]
-
-parser = argparse.ArgumentParser()
-parser.add_argument("files1", nargs=2)
-parser.add_argument("files2", nargs="*")
-args = parser.parse_args(["a", "b"])
-assert args.files1 == ["a", "b"] and args.files2 == []
-args = parser.parse_args(["a", "b", "c"])
-assert args.files1 == ["a", "b"] and args.files2 == ["c"]
diff --git a/asyncio_slow/asyncio_slow.py b/asyncio_slow/asyncio_slow.py
deleted file mode 100644
index 55f1347f1..000000000
--- a/asyncio_slow/asyncio_slow.py
+++ /dev/null
@@ -1,151 +0,0 @@
-import time
-import logging
-
-
-log = logging.getLogger("asyncio")
-
-
-# Workaround for not being able to subclass builtin types
-class LoopStop(Exception):
-    pass
-
-class InvalidStateError(Exception):
-    pass
-
-# Object not matching any other object
-_sentinel = []
-
-
-class EventLoop:
-
-    def __init__(self):
-        self.q = []
-
-    def call_soon(self, c, *args):
-        self.q.append((c, args))
-
-    def call_later(self, delay, c, *args):
-        def _delayed(c, args, delay):
-            yield from sleep(delay)
-            self.call_soon(c, *args)
-        Task(_delayed(c, args, delay))
-
-    def run_forever(self):
-        while self.q:
-            c = self.q.pop(0)
-            try:
-                c[0](*c[1])
-            except LoopStop:
-                return
-        # I mean, forever
-        while True:
-            time.sleep(1)
-
-    def stop(self):
-        def _cb():
-            raise LoopStop
-        self.call_soon(_cb)
-
-    def run_until_complete(self, coro):
-        t = async(coro)
-        t.add_done_callback(lambda a: self.stop())
-        self.run_forever()
-
-    def close(self):
-        pass
-
-
-_def_event_loop = EventLoop()
-
-
-class Future:
-
-    def __init__(self, loop=_def_event_loop):
-        self.loop = loop
-        self.res = _sentinel
-        self.cbs = []
-
-    def result(self):
-        if self.res is _sentinel:
-            raise InvalidStateError
-        return self.res
-
-    def add_done_callback(self, fn):
-        if self.res is _sentinel:
-            self.cbs.append(fn)
-        else:
-            self.loop.call_soon(fn, self)
-
-    def set_result(self, val):
-        self.res = val
-        for f in self.cbs:
-            f(self)
-
-
-class Task(Future):
-
-    def __init__(self, coro, loop=_def_event_loop):
-        super().__init__()
-        self.loop = loop
-        self.c = coro
-        # upstream asyncio forces task to be scheduled on instantiation
-        self.loop.call_soon(self)
-
-    def __call__(self):
-        try:
-            next(self.c)
-            self.loop.call_soon(self)
-        except StopIteration as e:
-            log.debug("Coro finished: %s", self.c)
-            self.set_result(None)
-
-
-def get_event_loop():
-    return _def_event_loop
-
-
-# Decorator
-def coroutine(f):
-    return f
-
-
-def async(coro):
-    if isinstance(coro, Future):
-        return coro
-    return Task(coro)
-
-
-class _Wait(Future):
-
-    def __init__(self, n):
-        Future.__init__(self)
-        self.n = n
-
-    def _done(self):
-        self.n -= 1
-        log.debug("Wait: remaining tasks: %d", self.n)
-        if not self.n:
-            self.set_result(None)
-
-    def __call__(self):
-        pass
-
-
-def wait(coro_list, loop=_def_event_loop):
-
-    w = _Wait(len(coro_list))
-
-    for c in coro_list:
-        t = async(c)
-        t.add_done_callback(lambda val: w._done())
-
-    return w
-
-
-def sleep(secs):
-    t = time.time()
-    log.debug("Started sleep at: %s, targetting: %s", t, t + secs)
-    while time.time() < t + secs:
-        time.sleep(0.01)
-        yield
-    log.debug("Finished sleeping %ss", secs)
diff --git a/asyncio_slow/test_chain.py b/asyncio_slow/test_chain.py
deleted file mode 100644
index 8d6b9a615..000000000
--- a/asyncio_slow/test_chain.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#https://docs.python.org/3.4/library/asyncio-task.html#example-chain-coroutines
-#import asyncio
-import asyncio_slow as asyncio
-
-@asyncio.coroutine
-def compute(x, y):
-    print("Compute %s + %s ..." % (x, y))
-    yield from asyncio.sleep(1.0)
-    return x + y
-
-@asyncio.coroutine
-def print_sum(x, y):
-    result = yield from compute(x, y)
-    print("%s + %s = %s" % (x, y, result))
-
-loop = asyncio.get_event_loop()
-loop.run_until_complete(print_sum(1, 2))
-loop.close()
diff --git a/asyncio_slow/test_future.py b/asyncio_slow/test_future.py
deleted file mode 100644
index 53026c8d0..000000000
--- a/asyncio_slow/test_future.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#https://docs.python.org/3.4/library/asyncio-task.html#example-chain-coroutines
-#import asyncio
-import asyncio_slow as asyncio
-
-@asyncio.coroutine
-def slow_operation(future):
-    yield from asyncio.sleep(1)
-    future.set_result('Future is done!')
-
-loop = asyncio.get_event_loop()
-future = asyncio.Future()
-asyncio.Task(slow_operation(future))
-loop.run_until_complete(future)
-print(future.result())
-loop.close()
diff --git a/asyncio_slow/test_future2.py b/asyncio_slow/test_future2.py
deleted file mode 100644
index 8ba03ef85..000000000
--- a/asyncio_slow/test_future2.py
+++ /dev/null
@@ -1,21 +0,0 @@
-#https://docs.python.org/3.4/library/asyncio-task.html#example-future-with-run-forever
-#import asyncio
-import asyncio_slow as asyncio
-
-@asyncio.coroutine
-def slow_operation(future):
-    yield from asyncio.sleep(1)
-    future.set_result('Future is done!')
-
-def got_result(future):
-    print(future.result())
-    loop.stop()
-
-loop = asyncio.get_event_loop()
-future = asyncio.Future()
-asyncio.Task(slow_operation(future))
-future.add_done_callback(got_result)
-try:
-    loop.run_forever()
-finally:
-    loop.close()
\ No newline at end of file
diff --git a/asyncio_slow/test_hello_world.py b/asyncio_slow/test_hello_world.py
deleted file mode 100644
index fab558134..000000000
--- a/asyncio_slow/test_hello_world.py
+++ /dev/null
@@ -1,12 +0,0 @@
-#https://docs.python.org/3.4/library/asyncio-task.html#example-hello-world-coroutine
-#import asyncio
-import asyncio_slow as asyncio
-
-@asyncio.coroutine
-def greet_every_two_seconds():
-    while True:
-        print('Hello World')
-        yield from asyncio.sleep(2)
-
-loop = asyncio.get_event_loop()
-loop.run_until_complete(greet_every_two_seconds())
diff --git a/asyncio_slow/test_hello_world_bare.py b/asyncio_slow/test_hello_world_bare.py
deleted file mode 100644
index 1f8d9702f..000000000
--- a/asyncio_slow/test_hello_world_bare.py
+++ /dev/null
@@ -1,12 +0,0 @@
-#import asyncio
-import asyncio_slow as asyncio
-
-@asyncio.coroutine
-def greet_every_two_seconds():
-    while True:
-        print('Hello World')
-        yield from asyncio.sleep(2)
-
-loop = asyncio.get_event_loop()
-asyncio.Task(greet_every_two_seconds())
-loop.run_forever()
diff --git a/asyncio_slow/test_hello_world_callback.py b/asyncio_slow/test_hello_world_callback.py
deleted file mode 100644
index 9836ffd7b..000000000
--- a/asyncio_slow/test_hello_world_callback.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# https://docs.python.org/3.4/library/asyncio-eventloop.html#example-hello-world-callback
-#import asyncio
-import asyncio_slow as asyncio
-
-def print_and_repeat(loop):
-    print('Hello World')
-    loop.call_later(2, print_and_repeat, loop)
-
-loop = asyncio.get_event_loop()
-loop.call_soon(print_and_repeat, loop)
-loop.run_forever()
diff --git a/asyncio_slow/test_parallel.py b/asyncio_slow/test_parallel.py
deleted file mode 100644
index 48a187b87..000000000
--- a/asyncio_slow/test_parallel.py
+++ /dev/null
@@ -1,21 +0,0 @@
-#https://docs.python.org/3.4/library/asyncio-task.html#example-parallel-execution-of-tasks
-#import asyncio
-import asyncio_slow as asyncio
-
-@asyncio.coroutine
-def factorial(name, number):
-    f = 1
-    for i in range(2, number+1):
-        print("Task %s: Compute factorial(%s)..." % (name, i))
-        yield from asyncio.sleep(1)
-        f *= i
-    print("Task %s: factorial(%s) = %s" % (name, number, f))
-
-tasks = [
-    asyncio.Task(factorial("A", 2)),
-    asyncio.Task(factorial("B", 3)),
-    asyncio.Task(factorial("C", 4))]
-
-loop = asyncio.get_event_loop()
-loop.run_until_complete(asyncio.wait(tasks))
-loop.close()
diff --git a/base64/base64.py b/base64/base64.py
deleted file mode 100644
index be01ba97c..000000000
--- a/base64/base64.py
+++ /dev/null
@@ -1,414 +0,0 @@
-#! /usr/bin/env python3
-
-"""RFC 3548: Base16, Base32, Base64 Data Encodings"""
-
-# Modified 04-Oct-1995 by Jack Jansen to use binascii module
-# Modified 30-Dec-2003 by Barry Warsaw to add full RFC 3548 support
-# Modified 22-May-2007 by Guido van Rossum to use bytes everywhere
-
-import re
-import struct
-import binascii
-
-
-__all__ = [
-    # Legacy interface exports traditional RFC 1521 Base64 encodings
-    'encode', 'decode', 'encodebytes', 'decodebytes',
-    # Generalized interface for other encodings
-    'b64encode', 'b64decode', 'b32encode', 'b32decode',
-    'b16encode', 'b16decode',
-    # Standard Base64 encoding
-    'standard_b64encode', 'standard_b64decode',
-    # Some common Base64 alternatives.  As referenced by RFC 3458, see thread
-    # starting at:
-    #
-    # http://zgp.org/pipermail/p2p-hackers/2001-September/000316.html
-    'urlsafe_b64encode', 'urlsafe_b64decode',
-    ]
-
-
-bytes_types = (bytes, bytearray)  # Types acceptable as binary data
-
-def _bytes_from_decode_data(s):
-    if isinstance(s, str):
-        try:
-            return s.encode('ascii')
-#        except UnicodeEncodeError:
-        except:
-            raise ValueError('string argument should contain only ASCII characters')
-    elif isinstance(s, bytes_types):
-        return s
-    else:
-        raise TypeError("argument should be bytes or ASCII string, not %s" % s.__class__.__name__)
-
-
-
-# Base64 encoding/decoding uses binascii
-
-def b64encode(s, altchars=None):
-    """Encode a byte string using Base64.
-
-    s is the byte string to encode.  Optional altchars must be a byte
-    string of length 2 which specifies an alternative alphabet for the
-    '+' and '/' characters.  This allows an application to
-    e.g. generate url or filesystem safe Base64 strings.
-
-    The encoded byte string is returned.
-    """
-    if not isinstance(s, bytes_types):
-        raise TypeError("expected bytes, not %s" % s.__class__.__name__)
-    # Strip off the trailing newline
-    encoded = binascii.b2a_base64(s)[:-1]
-    if altchars is not None:
-        if not isinstance(altchars, bytes_types):
-            raise TypeError("expected bytes, not %s"
-                            % altchars.__class__.__name__)
-        assert len(altchars) == 2, repr(altchars)
-        return encoded.translate(bytes.maketrans(b'+/', altchars))
-    return encoded
-
-
-def b64decode(s, altchars=None, validate=False):
-    """Decode a Base64 encoded byte string.
-
-    s is the byte string to decode.  Optional altchars must be a
-    string of length 2 which specifies the alternative alphabet used
-    instead of the '+' and '/' characters.
-
-    The decoded string is returned.  A binascii.Error is raised if s is
-    incorrectly padded.
-
-    If validate is False (the default), non-base64-alphabet characters are
-    discarded prior to the padding check.  If validate is True,
-    non-base64-alphabet characters in the input result in a binascii.Error.
-    """
-    s = _bytes_from_decode_data(s)
-    if altchars is not None:
-        altchars = _bytes_from_decode_data(altchars)
-        assert len(altchars) == 2, repr(altchars)
-        s = s.translate(bytes.maketrans(altchars, b'+/'))
-    if validate and not re.match(b'^[A-Za-z0-9+/]*={0,2}$', s):
-        raise binascii.Error('Non-base64 digit found')
-    return binascii.a2b_base64(s)
-
-
-def standard_b64encode(s):
-    """Encode a byte string using the standard Base64 alphabet.
-
-    s is the byte string to encode.  The encoded byte string is returned.
-    """
-    return b64encode(s)
-
-def standard_b64decode(s):
-    """Decode a byte string encoded with the standard Base64 alphabet.
-
-    s is the byte string to decode.  The decoded byte string is
-    returned.  binascii.Error is raised if the input is incorrectly
-    padded or if there are non-alphabet characters present in the
-    input.
-    """
-    return b64decode(s)
-
-
-#_urlsafe_encode_translation = bytes.maketrans(b'+/', b'-_')
-#_urlsafe_decode_translation = bytes.maketrans(b'-_', b'+/')
-
-def urlsafe_b64encode(s):
-    """Encode a byte string using a url-safe Base64 alphabet.
-
-    s is the byte string to encode.  The encoded byte string is
-    returned.  The alphabet uses '-' instead of '+' and '_' instead of
-    '/'.
-    """
-#    return b64encode(s).translate(_urlsafe_encode_translation)
-    raise NotImplementedError()
-
-def urlsafe_b64decode(s):
-    """Decode a byte string encoded with the standard Base64 alphabet.
-
-    s is the byte string to decode.  The decoded byte string is
-    returned.  binascii.Error is raised if the input is incorrectly
-    padded or if there are non-alphabet characters present in the
-    input.
-
-    The alphabet uses '-' instead of '+' and '_' instead of '/'.
-    """
-#    s = _bytes_from_decode_data(s)
-#    s = s.translate(_urlsafe_decode_translation)
-#    return b64decode(s)
-    raise NotImplementedError()
-
-
-
-# Base32 encoding/decoding must be done in Python
-_b32alphabet = {
-    0: b'A',  9: b'J', 18: b'S', 27: b'3',
-    1: b'B', 10: b'K', 19: b'T', 28: b'4',
-    2: b'C', 11: b'L', 20: b'U', 29: b'5',
-    3: b'D', 12: b'M', 21: b'V', 30: b'6',
-    4: b'E', 13: b'N', 22: b'W', 31: b'7',
-    5: b'F', 14: b'O', 23: b'X',
-    6: b'G', 15: b'P', 24: b'Y',
-    7: b'H', 16: b'Q', 25: b'Z',
-    8: b'I', 17: b'R', 26: b'2',
-    }
-
-_b32tab = [v[0] for k, v in sorted(_b32alphabet.items())]
-_b32rev = dict([(v[0], k) for k, v in _b32alphabet.items()])
-
-
-def b32encode(s):
-    """Encode a byte string using Base32.
-
-    s is the byte string to encode.  The encoded byte string is returned.
-    """
-    if not isinstance(s, bytes_types):
-        raise TypeError("expected bytes, not %s" % s.__class__.__name__)
-    quanta, leftover = divmod(len(s), 5)
-    # Pad the last quantum with zero bits if necessary
-    if leftover:
-        s = s + bytes(5 - leftover)  # Don't use += !
-        quanta += 1
-    encoded = bytearray()
-    for i in range(quanta):
-        # c1 and c2 are 16 bits wide, c3 is 8 bits wide.  The intent of this
-        # code is to process the 40 bits in units of 5 bits.  So we take the 1
-        # leftover bit of c1 and tack it onto c2.  Then we take the 2 leftover
-        # bits of c2 and tack them onto c3.  The shifts and masks are intended
-        # to give us values of exactly 5 bits in width.
-        c1, c2, c3 = struct.unpack('!HHB', s[i*5:(i+1)*5])
-        c2 += (c1 & 1) << 16 # 17 bits wide
-        c3 += (c2 & 3) << 8  # 10 bits wide
-        encoded += bytes([_b32tab[c1 >> 11],         # bits 1 - 5
-                          _b32tab[(c1 >> 6) & 0x1f], # bits 6 - 10
-                          _b32tab[(c1 >> 1) & 0x1f], # bits 11 - 15
-                          _b32tab[c2 >> 12],         # bits 16 - 20 (1 - 5)
-                          _b32tab[(c2 >> 7) & 0x1f], # bits 21 - 25 (6 - 10)
-                          _b32tab[(c2 >> 2) & 0x1f], # bits 26 - 30 (11 - 15)
-                          _b32tab[c3 >> 5],          # bits 31 - 35 (1 - 5)
-                          _b32tab[c3 & 0x1f],        # bits 36 - 40 (1 - 5)
-                          ])
-    # Adjust for any leftover partial quanta
-    if leftover == 1:
-        encoded = encoded[:-6] + b'======'
-    elif leftover == 2:
-        encoded = encoded[:-4] + b'===='
-    elif leftover == 3:
-        encoded = encoded[:-3] + b'==='
-    elif leftover == 4:
-        encoded = encoded[:-1] + b'='
-    return bytes(encoded)
-
-
-def b32decode(s, casefold=False, map01=None):
-    """Decode a Base32 encoded byte string.
-
-    s is the byte string to decode.  Optional casefold is a flag
-    specifying whether a lowercase alphabet is acceptable as input.
-    For security purposes, the default is False.
-
-    RFC 3548 allows for optional mapping of the digit 0 (zero) to the
-    letter O (oh), and for optional mapping of the digit 1 (one) to
-    either the letter I (eye) or letter L (el).  The optional argument
-    map01 when not None, specifies which letter the digit 1 should be
-    mapped to (when map01 is not None, the digit 0 is always mapped to
-    the letter O).  For security purposes the default is None, so that
-    0 and 1 are not allowed in the input.
-
-    The decoded byte string is returned.  binascii.Error is raised if
-    the input is incorrectly padded or if there are non-alphabet
-    characters present in the input.
-    """
-    s = _bytes_from_decode_data(s)
-    quanta, leftover = divmod(len(s), 8)
-    if leftover:
-        raise binascii.Error('Incorrect padding')
-    # Handle section 2.4 zero and one mapping.  The flag map01 will be either
-    # False, or the character to map the digit 1 (one) to.  It should be
-    # either L (el) or I (eye).
-    if map01 is not None:
-        map01 = _bytes_from_decode_data(map01)
-        assert len(map01) == 1, repr(map01)
-        s = s.translate(bytes.maketrans(b'01', b'O' + map01))
-    if casefold:
-        s = s.upper()
-    # Strip off pad characters from the right.  We need to count the pad
-    # characters because this will tell us how many null bytes to remove from
-    # the end of the decoded string.
-    padchars = s.find(b'=')
-    if padchars > 0:
-        padchars = len(s) - padchars
-        s = s[:-padchars]
-    else:
-        padchars = 0
-
-    # Now decode the full quanta
-    parts = []
-    acc = 0
-    shift = 35
-    for c in s:
-        val = _b32rev.get(c)
-        if val is None:
-            raise binascii.Error('Non-base32 digit found')
-        acc += _b32rev[c] << shift
-        shift -= 5
-        if shift < 0:
-            parts.append(binascii.unhexlify(bytes('%010x' % acc, "ascii")))
-            acc = 0
-            shift = 35
-    # Process the last, partial quanta
-    last = binascii.unhexlify(bytes('%010x' % acc, "ascii"))
-    if padchars == 0:
-        last = b''                      # No characters
-    elif padchars == 1:
-        last = last[:-1]
-    elif padchars == 3:
-        last = last[:-2]
-    elif padchars == 4:
-        last = last[:-3]
-    elif padchars == 6:
-        last = last[:-4]
-    else:
-        raise binascii.Error('Incorrect padding')
-    parts.append(last)
-    return b''.join(parts)
-
-
-
-# RFC 3548, Base 16 Alphabet specifies uppercase, but hexlify() returns
-# lowercase.  The RFC also recommends against accepting input case
-# insensitively.
-def b16encode(s):
-    """Encode a byte string using Base16.
-
-    s is the byte string to encode.  The encoded byte string is returned.
-    """
-    if not isinstance(s, bytes_types):
-        raise TypeError("expected bytes, not %s" % s.__class__.__name__)
-    return binascii.hexlify(s).upper()
-
-
-def b16decode(s, casefold=False):
-    """Decode a Base16 encoded byte string.
-
-    s is the byte string to decode.  Optional casefold is a flag
-    specifying whether a lowercase alphabet is acceptable as input.
-    For security purposes, the default is False.
-
-    The decoded byte string is returned.  binascii.Error is raised if
-    s were incorrectly padded or if there are non-alphabet characters
-    present in the string.
-    """
-    s = _bytes_from_decode_data(s)
-    if casefold:
-        s = s.upper()
-    if re.search(b'[^0-9A-F]', s):
-        raise binascii.Error('Non-base16 digit found')
-    return binascii.unhexlify(s)
-
-
-
-# Legacy interface.  This code could be cleaned up since I don't believe
-# binascii has any line length limitations.  It just doesn't seem worth it
-# though.  The files should be opened in binary mode.
-
-MAXLINESIZE = 76 # Excluding the CRLF
-MAXBINSIZE = (MAXLINESIZE//4)*3
-
-def encode(input, output):
-    """Encode a file; input and output are binary files."""
-    while True:
-        s = input.read(MAXBINSIZE)
-        if not s:
-            break
-        while len(s) < MAXBINSIZE:
-            ns = input.read(MAXBINSIZE-len(s))
-            if not ns:
-                break
-            s += ns
-        line = binascii.b2a_base64(s)
-        output.write(line)
-
-
-def decode(input, output):
-    """Decode a file; input and output are binary files."""
-    while True:
-        line = input.readline()
-        if not line:
-            break
-        s = binascii.a2b_base64(line)
-        output.write(s)
-
-
-def encodebytes(s):
-    """Encode a bytestring into a bytestring containing multiple lines
-    of base-64 data."""
-    if not isinstance(s, bytes_types):
-        raise TypeError("expected bytes, not %s" % s.__class__.__name__)
-    pieces = []
-    for i in range(0, len(s), MAXBINSIZE):
-        chunk = s[i : i + MAXBINSIZE]
-        pieces.append(binascii.b2a_base64(chunk))
-    return b"".join(pieces)
-
-def encodestring(s):
-    """Legacy alias of encodebytes()."""
-    import warnings
-    warnings.warn("encodestring() is a deprecated alias, use encodebytes()",
-                  DeprecationWarning, 2)
-    return encodebytes(s)
-
-
-def decodebytes(s):
-    """Decode a bytestring of base-64 data into a bytestring."""
-    if not isinstance(s, bytes_types):
-        raise TypeError("expected bytes, not %s" % s.__class__.__name__)
-    return binascii.a2b_base64(s)
-
-def decodestring(s):
-    """Legacy alias of decodebytes()."""
-    import warnings
-    warnings.warn("decodestring() is a deprecated alias, use decodebytes()",
-                  DeprecationWarning, 2)
-    return decodebytes(s)
-
-
-# Usable as a script...
-def main():
-    """Small main program"""
-    import sys, getopt
-    try:
-        opts, args = getopt.getopt(sys.argv[1:], 'deut')
-    except getopt.error as msg:
-        sys.stdout = sys.stderr
-        print(msg)
-        print("""usage: %s [-d|-e|-u|-t] [file|-]
-        -d, -u: decode
-        -e: encode (default)
-        -t: encode and decode string 'Aladdin:open sesame'"""%sys.argv[0])
-        sys.exit(2)
-    func = encode
-    for o, a in opts:
-        if o == '-e': func = encode
-        if o == '-d': func = decode
-        if o == '-u': func = decode
-        if o == '-t': test(); return
-    if args and args[0] != '-':
-        with open(args[0], 'rb') as f:
-            func(f, sys.stdout.buffer)
-    else:
-        func(sys.stdin.buffer, sys.stdout.buffer)
-
-
-def test():
-    s0 = b"Aladdin:open sesame"
-    print(repr(s0))
-    s1 = encodebytes(s0)
-    print(repr(s1))
-    s2 = decodebytes(s1)
-    print(repr(s2))
-    assert s0 == s2
-
-
-if __name__ == '__main__':
-    main()
diff --git a/base64/metadata.txt b/base64/metadata.txt
deleted file mode 100644
index 853a2f992..000000000
--- a/base64/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype=cpython
-type=module
-version=3.3.3-2
-depends = struct
\ No newline at end of file
diff --git a/base64/setup.py b/base64/setup.py
deleted file mode 100644
index c751ff23e..000000000
--- a/base64/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-base64',
-      version='3.3.3-2',
-      description='CPython base64 module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['base64'],
-      install_requires=['micropython-struct'])
diff --git a/base64/test_base64.py b/base64/test_base64.py
deleted file mode 100644
index 36abfbfcb..000000000
--- a/base64/test_base64.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import base64
-
-b = base64.b64encode(b'zlutoucky kun upel dabelske ody')
-print(b)
-
-if b != b'emx1dG91Y2t5IGt1biB1cGVsIGRhYmVsc2tlIG9keQ==':
-    raise Exception("Error")
-
-d = base64.b64decode(b)
-print(d)
-
-if d != b'zlutoucky kun upel dabelske ody':
-    raise Exception("Error")
-
-base64.test()
-
-binary = b'\x99\x10\xaa'
-b = base64.b64encode(binary)
-if b != b'mRCq':
-    raise Exception("Error")
-
-d = base64.b64decode(b)
-print(d)
-if d != binary:
-    raise Exception("Error")
-
-d = base64.b32encode(b'zlutoucky kun upel dabelske ody')
-if d != b'PJWHK5DPOVRWW6JANN2W4IDVOBSWYIDEMFRGK3DTNNSSA33EPE======':
-    raise Exception("Error")
-
-print(d)
-b = base64.b32decode(d)
-if b != b'zlutoucky kun upel dabelske ody':
-    raise Exception("Error")
-
-print("OK")
diff --git a/binascii/binascii.py b/binascii/binascii.py
deleted file mode 100644
index 2b7cf6fe6..000000000
--- a/binascii/binascii.py
+++ /dev/null
@@ -1,113 +0,0 @@
-from ubinascii import *
-
-if not "unhexlify" in globals():
-    def unhexlify(data):
-        if len(data) % 2 != 0:
-            raise ValueError("Odd-length string")
-
-        return bytes([ int(data[i:i+2], 16) for i in range(0, len(data), 2) ])
-
-b2a_hex = hexlify
-a2b_hex = unhexlify
-
-# ____________________________________________________________
-
-PAD = '='
-
-table_a2b_base64 = [
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
-    52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1,-1,-1,-1, # Note PAD->-1 here
-    -1, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,
-    15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
-    -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
-    41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,    
-]
-def _transform(n):
-    if n == -1:
-        return '\xff'
-    else:
-        return chr(n)
-table_a2b_base64 = ''.join(map(_transform, table_a2b_base64))
-assert len(table_a2b_base64) == 256
-
-def a2b_base64(ascii):
-    "Decode a line of base64 data."
-
-    res = []
-    quad_pos = 0
-    leftchar = 0
-    leftbits = 0
-    last_char_was_a_pad = False
-
-    for c in ascii:
-        c = chr(c)
-        if c == PAD:
-            if quad_pos > 2 or (quad_pos == 2 and last_char_was_a_pad):
-                break      # stop on 'xxx=' or on 'xx=='
-            last_char_was_a_pad = True
-        else:
-            n = ord(table_a2b_base64[ord(c)])
-            if n == 0xff:
-                continue    # ignore strange characters
-            #
-            # Shift it in on the low end, and see if there's
-            # a byte ready for output.
-            quad_pos = (quad_pos + 1) & 3
-            leftchar = (leftchar << 6) | n
-            leftbits += 6
-            #
-            if leftbits >= 8:
-                leftbits -= 8
-                res.append((leftchar >> leftbits).to_bytes(1))
-                leftchar &= ((1 << leftbits) - 1)
-            #
-            last_char_was_a_pad = False
-    else:
-        if leftbits != 0:
-            raise Exception("Incorrect padding")
-
-    return b''.join(res)
-
-# ____________________________________________________________
-
-table_b2a_base64 = (
-    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
-
-def b2a_base64(bin):
-    "Base64-code line of data."
-
-    newlength = (len(bin) + 2) // 3
-    newlength = newlength * 4 + 1
-    res = []
-
-    leftchar = 0
-    leftbits = 0
-    for c in bin:
-        # Shift into our buffer, and output any 6bits ready
-        leftchar = (leftchar << 8) | c
-        leftbits += 8
-        res.append(table_b2a_base64[(leftchar >> (leftbits-6)) & 0x3f])
-        leftbits -= 6
-        if leftbits >= 6:
-            res.append(table_b2a_base64[(leftchar >> (leftbits-6)) & 0x3f])
-            leftbits -= 6
-    #
-    if leftbits == 2:
-        res.append(table_b2a_base64[(leftchar & 3) << 4])
-        res.append(PAD)
-        res.append(PAD)
-    elif leftbits == 4:
-        res.append(table_b2a_base64[(leftchar & 0xf) << 2])
-        res.append(PAD)
-    res.append('\n')
-    return ''.join(res).encode('ascii')
diff --git a/binascii/metadata.txt b/binascii/metadata.txt
deleted file mode 100644
index 97100aaca..000000000
--- a/binascii/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=pypy
-type=module
-version=2.4.0-3
diff --git a/binascii/setup.py b/binascii/setup.py
deleted file mode 100644
index f15ab615f..000000000
--- a/binascii/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-binascii',
-      version='2.4.0-3',
-      description='PyPy binascii module ported to MicroPython',
-      long_description='This is a module ported from PyPy standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='PyPy Developers',
-      author_email='pypy-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['binascii'])
diff --git a/binascii/test_binascii.py b/binascii/test_binascii.py
deleted file mode 100644
index 6d89acba6..000000000
--- a/binascii/test_binascii.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from binascii import hexlify, unhexlify
-import utime
-
-data = b'zlutoucky kun upel dabelske ody'
-h = hexlify(data)
-
-if h != b'7a6c75746f75636b79206b756e207570656c20646162656c736b65206f6479':
-    raise Exception("Error")
-
-data2 = unhexlify(h)
-
-if data2 != data:
-    raise Exception("Error")
-
-start = utime.time()
-for x in range(100000):
-    d = unhexlify(h)
-
-print("100000 iterations in: " + str(utime.time() - start))
-
-print("OK")
diff --git a/binhex/binhex.py b/binhex/binhex.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/binhex/metadata.txt b/binhex/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/binhex/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/binhex/setup.py b/binhex/setup.py
deleted file mode 100644
index 50cc72d28..000000000
--- a/binhex/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-binhex',
-      version='0.0.1',
-      description='Dummy binhex module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['binhex'])
diff --git a/bisect/bisect.py b/bisect/bisect.py
deleted file mode 100644
index 4a4d05255..000000000
--- a/bisect/bisect.py
+++ /dev/null
@@ -1,92 +0,0 @@
-"""Bisection algorithms."""
-
-def insort_right(a, x, lo=0, hi=None):
-    """Insert item x in list a, and keep it sorted assuming a is sorted.
-
-    If x is already in a, insert it to the right of the rightmost x.
-
-    Optional args lo (default 0) and hi (default len(a)) bound the
-    slice of a to be searched.
-    """
-
-    if lo < 0:
-        raise ValueError('lo must be non-negative')
-    if hi is None:
-        hi = len(a)
-    while lo < hi:
-        mid = (lo+hi)//2
-        if x < a[mid]: hi = mid
-        else: lo = mid+1
-    a.insert(lo, x)
-
-insort = insort_right   # backward compatibility
-
-def bisect_right(a, x, lo=0, hi=None):
-    """Return the index where to insert item x in list a, assuming a is sorted.
-
-    The return value i is such that all e in a[:i] have e <= x, and all e in
-    a[i:] have e > x.  So if x already appears in the list, a.insert(x) will
-    insert just after the rightmost x already there.
-
-    Optional args lo (default 0) and hi (default len(a)) bound the
-    slice of a to be searched.
-    """
-
-    if lo < 0:
-        raise ValueError('lo must be non-negative')
-    if hi is None:
-        hi = len(a)
-    while lo < hi:
-        mid = (lo+hi)//2
-        if x < a[mid]: hi = mid
-        else: lo = mid+1
-    return lo
-
-bisect = bisect_right   # backward compatibility
-
-def insort_left(a, x, lo=0, hi=None):
-    """Insert item x in list a, and keep it sorted assuming a is sorted.
-
-    If x is already in a, insert it to the left of the leftmost x.
-
-    Optional args lo (default 0) and hi (default len(a)) bound the
-    slice of a to be searched.
-    """
-
-    if lo < 0:
-        raise ValueError('lo must be non-negative')
-    if hi is None:
-        hi = len(a)
-    while lo < hi:
-        mid = (lo+hi)//2
-        if a[mid] < x: lo = mid+1
-        else: hi = mid
-    a.insert(lo, x)
-
-
-def bisect_left(a, x, lo=0, hi=None):
-    """Return the index where to insert item x in list a, assuming a is sorted.
-
-    The return value i is such that all e in a[:i] have e < x, and all e in
-    a[i:] have e >= x.  So if x already appears in the list, a.insert(x) will
-    insert just before the leftmost x already there.
-
-    Optional args lo (default 0) and hi (default len(a)) bound the
-    slice of a to be searched.
-    """
-
-    if lo < 0:
-        raise ValueError('lo must be non-negative')
-    if hi is None:
-        hi = len(a)
-    while lo < hi:
-        mid = (lo+hi)//2
-        if a[mid] < x: lo = mid+1
-        else: hi = mid
-    return lo
-
-# Overwrite above definitions with a fast C implementation
-try:
-    from _bisect import *
-except ImportError:
-    pass
diff --git a/bisect/setup.py b/bisect/setup.py
deleted file mode 100644
index 10d782e53..000000000
--- a/bisect/setup.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise distutils will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-def desc_dummy(name):
-    return 'Dummy %s module to MicroPython' % name
-def desc_cpython(name):
-    return 'CPython %s module ported to MicroPython' % name
-
-NAME = 'bisect'
-
-setup(name='micropython-' + NAME,
-      version='0.5',
-      description=desc_cpython(NAME),
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=[NAME])
diff --git a/calendar/calendar.py b/calendar/calendar.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/calendar/metadata.txt b/calendar/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/calendar/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/calendar/setup.py b/calendar/setup.py
deleted file mode 100644
index 3145da908..000000000
--- a/calendar/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-calendar',
-      version='0.0.0',
-      description='Dummy calendar module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['calendar'])
diff --git a/cgi/metadata.txt b/cgi/metadata.txt
deleted file mode 100644
index 5cd44f10e..000000000
--- a/cgi/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=cpython
-type=module
-version=3.3.3-1
diff --git a/cgi/setup.py b/cgi/setup.py
deleted file mode 100644
index 1cff29650..000000000
--- a/cgi/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-cgi',
-      version='3.3.3-1',
-      description='CPython cgi module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['cgi'])
diff --git a/cmd/metadata.txt b/cmd/metadata.txt
deleted file mode 100644
index 17db50596..000000000
--- a/cmd/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = cpython
-type = module
-version = 3.4.0-1
diff --git a/cmd/setup.py b/cmd/setup.py
deleted file mode 100644
index 5bf2395ec..000000000
--- a/cmd/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-cmd',
-      version='3.4.0-1',
-      description='CPython cmd module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['cmd'])
diff --git a/collections.defaultdict/metadata.txt b/collections.defaultdict/metadata.txt
deleted file mode 100644
index 955962b4f..000000000
--- a/collections.defaultdict/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = micropython-lib
-type = package
-version = 0.2.1
-author = Paul Sokolovsky
diff --git a/collections.defaultdict/setup.py b/collections.defaultdict/setup.py
deleted file mode 100644
index ec843ce51..000000000
--- a/collections.defaultdict/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-collections.defaultdict',
-      version='0.2.1',
-      description='collections.defaultdict module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['collections'])
diff --git a/collections.defaultdict/test_defaultdict.py b/collections.defaultdict/test_defaultdict.py
deleted file mode 100644
index eb7a33140..000000000
--- a/collections.defaultdict/test_defaultdict.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from collections import defaultdict
-
-d = defaultdict.defaultdict(lambda:42)
-assert d[1] == 42
-d[2] = 3
-assert d[2] == 3
-del d[1]
-assert d[1] == 42
diff --git a/collections.deque/collections/deque.py b/collections.deque/collections/deque.py
deleted file mode 100644
index b284e5f38..000000000
--- a/collections.deque/collections/deque.py
+++ /dev/null
@@ -1,37 +0,0 @@
-class deque:
-
-    def __init__(self, iterable=None):
-        if iterable is None:
-            self.q = []
-        else:
-            self.q = list(iterable)
-
-    def popleft(self):
-        return self.q.pop(0)
-
-    def popright(self):
-        return self.q.pop()
-
-    def pop(self):
-        return self.q.pop()
-
-    def append(self, a):
-        self.q.append(a)
-
-    def appendleft(self, a):
-        self.q.insert(0, a)
-
-    def extend(self, a):
-        self.q.extend(a)
-
-    def __len__(self):
-        return len(self.q)
-
-    def __bool__(self):
-        return bool(self.q)
-
-    def __iter__(self):
-        yield from self.q
-
-    def __str__(self):
-        return 'deque({})'.format(self.q)
diff --git a/collections.deque/metadata.txt b/collections.deque/metadata.txt
deleted file mode 100644
index c50f2e450..000000000
--- a/collections.deque/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = package
-version = 0.1.2
diff --git a/collections.deque/setup.py b/collections.deque/setup.py
deleted file mode 100644
index 87e05905b..000000000
--- a/collections.deque/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-collections.deque',
-      version='0.1.2',
-      description='collections.deque module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['collections'])
diff --git a/collections/collections/__init__.py b/collections/collections/__init__.py
deleted file mode 100644
index faf61a81c..000000000
--- a/collections/collections/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Should be reimplemented for MicroPython
-# Reason:
-# CPython implementation brings in metaclasses and other bloat.
-# This is going to be just import-all for other modules in a namespace package
-from _collections import *
-try:
-    from .defaultdict import defaultdict
-except ImportError:
-    pass
-try:
-    from .deque import deque
-except ImportError:
-    pass
-
-class MutableMapping:
-    pass
diff --git a/collections/metadata.txt b/collections/metadata.txt
deleted file mode 100644
index 42937eaee..000000000
--- a/collections/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = package
-version = 0.1
diff --git a/collections/setup.py b/collections/setup.py
deleted file mode 100644
index 1edc637af..000000000
--- a/collections/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-collections',
-      version='0.1',
-      description='collections module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['collections'])
diff --git a/concurrent.futures/concurrent/futures/__init__.py b/concurrent.futures/concurrent/futures/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/concurrent.futures/metadata.txt b/concurrent.futures/metadata.txt
deleted file mode 100644
index be0a5a7b1..000000000
--- a/concurrent.futures/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=package
-version=0.0.0
diff --git a/concurrent.futures/setup.py b/concurrent.futures/setup.py
deleted file mode 100644
index 2ecdc1c1a..000000000
--- a/concurrent.futures/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-concurrent.futures',
-      version='0.0.0',
-      description='Dummy concurrent.futures module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['concurrent'])
diff --git a/contextlib/metadata.txt b/contextlib/metadata.txt
deleted file mode 100644
index c35c8280c..000000000
--- a/contextlib/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = cpython
-type = module
-version = 3.4.2-3
-long_desc = Port of contextlib for micropython
-depends = ucontextlib, collections
diff --git a/contextlib/setup.py b/contextlib/setup.py
deleted file mode 100644
index ef0b72efa..000000000
--- a/contextlib/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-contextlib',
-      version='3.4.2-3',
-      description='CPython contextlib module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['contextlib'],
-      install_requires=['micropython-ucontextlib', 'micropython-collections'])
diff --git a/contextlib/tests.py b/contextlib/tests.py
deleted file mode 100644
index f6cbae1c8..000000000
--- a/contextlib/tests.py
+++ /dev/null
@@ -1,386 +0,0 @@
-import sys
-import unittest
-from contextlib import closing, suppress, ExitStack
-
-
-class ClosingTestCase(unittest.TestCase):
-
-    class Closable:
-        def __init__(self):
-            self.closed = False
-
-        def close(self):
-            self.closed = True
-
-    def test_closing(self):
-        closable = self.Closable()
-        with closing(closable) as c:
-            self.assertFalse(c.closed)
-        self.assertTrue(closable.closed)
-
-    def test_closing_after_error(self):
-        closable = self.Closable()
-        exc = Exception()
-        try:
-            with closing(closable) as c:
-                raise exc
-        except Exception as e:
-            self.assertEqual(exc, e)
-        self.assertTrue(closable.closed)
-
-
-class SuppressTestCase(unittest.TestCase):
-
-    def test_suppress(self):
-        with suppress(ValueError, TypeError):
-            raise ValueError()
-            raise TypeError()
-        self.assertTrue(True)
-
-
-class TestExitStack(unittest.TestCase):
-
-    # @support.requires_docstrings
-    def _test_instance_docs(self):
-        # Issue 19330: ensure context manager instances have good docstrings
-        cm_docstring = ExitStack.__doc__
-        obj = ExitStack()
-        self.assertEqual(obj.__doc__, cm_docstring)
-
-    def test_no_resources(self):
-        with ExitStack():
-            pass
-
-    def test_callback(self):
-        expected = [
-            ((), {}),
-            ((1,), {}),
-            ((1,2), {}),
-            ((), dict(example=1)),
-            ((1,), dict(example=1)),
-            ((1,2), dict(example=1)),
-        ]
-        result = []
-        def _exit(*args, **kwds):
-            """Test metadata propagation"""
-            result.append((args, kwds))
-        with ExitStack() as stack:
-            for args, kwds in reversed(expected):
-                if args and kwds:
-                    f = stack.callback(_exit, *args, **kwds)
-                elif args:
-                    f = stack.callback(_exit, *args)
-                elif kwds:
-                    f = stack.callback(_exit, **kwds)
-                else:
-                    f = stack.callback(_exit)
-                self.assertIs(f, _exit)
-            # for wrapper in stack._exit_callbacks:
-            #     self.assertIs(wrapper.__wrapped__, _exit)
-            #     self.assertNotEqual(wrapper.__name__, _exit.__name__)
-            #     self.assertIsNone(wrapper.__doc__, _exit.__doc__)
-        self.assertEqual(result, expected)
-
-    def test_push(self):
-        exc_raised = ZeroDivisionError
-        def _expect_exc(exc_type, exc, exc_tb):
-            self.assertIs(exc_type, exc_raised)
-        def _suppress_exc(*exc_details):
-            return True
-        def _expect_ok(exc_type, exc, exc_tb):
-            self.assertIsNone(exc_type)
-            self.assertIsNone(exc)
-            self.assertIsNone(exc_tb)
-        class ExitCM(object):
-            def __init__(self, check_exc):
-                self.check_exc = check_exc
-            def __enter__(self):
-                self.fail("Should not be called!")
-            def __exit__(self, *exc_details):
-                self.check_exc(*exc_details)
-        with ExitStack() as stack:
-            stack.push(_expect_ok)
-            self.assertIs(tuple(stack._exit_callbacks)[-1], _expect_ok)
-            cm = ExitCM(_expect_ok)
-            stack.push(cm)
-            # self.assertIs(stack._exit_callbacks[-1].__self__, cm)
-            stack.push(_suppress_exc)
-            self.assertIs(tuple(stack._exit_callbacks)[-1], _suppress_exc)
-            cm = ExitCM(_expect_exc)
-            stack.push(cm)
-            # self.assertIs(stack._exit_callbacks[-1].__self__, cm)
-            stack.push(_expect_exc)
-            self.assertIs(tuple(stack._exit_callbacks)[-1], _expect_exc)
-            stack.push(_expect_exc)
-            self.assertIs(tuple(stack._exit_callbacks)[-1], _expect_exc)
-            1/0
-
-    def test_enter_context(self):
-        class TestCM(object):
-            def __enter__(self):
-                result.append(1)
-            def __exit__(self, *exc_details):
-                result.append(3)
-
-        result = []
-        cm = TestCM()
-        with ExitStack() as stack:
-            @stack.callback  # Registered first => cleaned up last
-            def _exit():
-                result.append(4)
-            self.assertIsNotNone(_exit)
-            stack.enter_context(cm)
-            # self.assertIs(stack._exit_callbacks[-1].__self__, cm)
-            result.append(2)
-        self.assertEqual(result, [1, 2, 3, 4])
-
-    def test_close(self):
-        result = []
-        with ExitStack() as stack:
-            @stack.callback
-            def _exit():
-                result.append(1)
-            self.assertIsNotNone(_exit)
-            stack.close()
-            result.append(2)
-        self.assertEqual(result, [1, 2])
-
-    def test_pop_all(self):
-        result = []
-        with ExitStack() as stack:
-            @stack.callback
-            def _exit():
-                result.append(3)
-            self.assertIsNotNone(_exit)
-            new_stack = stack.pop_all()
-            result.append(1)
-        result.append(2)
-        new_stack.close()
-        self.assertEqual(result, [1, 2, 3])
-
-    def test_exit_raise(self):
-        with self.assertRaises(ZeroDivisionError):
-            with ExitStack() as stack:
-                stack.push(lambda *exc: False)
-                1/0
-
-    def test_exit_suppress(self):
-        with ExitStack() as stack:
-            stack.push(lambda *exc: True)
-            1/0
-
-    def test_exit_exception_chaining_reference(self):
-        # Sanity check to make sure that ExitStack chaining matches
-        # actual nested with statements
-        exc_chain = []
-        class RaiseExc:
-            def __init__(self, exc):
-                self.exc = exc
-            def __enter__(self):
-                return self
-            def __exit__(self, *exc_details):
-                exc_chain.append(exc_details[0])
-                raise self.exc
-
-        class RaiseExcWithContext:
-            def __init__(self, outer, inner):
-                self.outer = outer
-                self.inner = inner
-            def __enter__(self):
-                return self
-            def __exit__(self, *exc_details):
-                try:
-                    exc_chain.append(exc_details[0])
-                    raise self.inner
-                except:
-                    exc_chain.append(sys.exc_info()[0])
-                    raise self.outer
-
-        class SuppressExc:
-            def __enter__(self):
-                return self
-            def __exit__(self, *exc_details):
-                type(self).saved_details = exc_details
-                return True
-
-        try:
-            with RaiseExc(IndexError):
-                with RaiseExcWithContext(KeyError, AttributeError):
-                    with SuppressExc():
-                        with RaiseExc(ValueError):
-                            1 / 0
-        except IndexError as exc:
-            # self.assertIsInstance(exc.__context__, KeyError)
-            # self.assertIsInstance(exc.__context__.__context__, AttributeError)
-            # Inner exceptions were suppressed
-            # self.assertIsNone(exc.__context__.__context__.__context__)
-            exc_chain.append(type(exc))
-            assert tuple(exc_chain) == (ZeroDivisionError, None, AttributeError, KeyError, IndexError)
-        else:
-            self.fail("Expected IndexError, but no exception was raised")
-        # Check the inner exceptions
-        inner_exc = SuppressExc.saved_details[1]
-        self.assertIsInstance(inner_exc, ValueError)
-        # self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
-
-    def test_exit_exception_chaining(self):
-        # Ensure exception chaining matches the reference behaviour
-        exc_chain = []
-        def raise_exc(exc):
-            frame_exc = sys.exc_info()[0]
-            if frame_exc is not None:
-                exc_chain.append(frame_exc)
-            exc_chain.append(exc)
-            raise exc
-
-        saved_details = None
-        def suppress_exc(*exc_details):
-            nonlocal saved_details
-            saved_details = exc_details
-            assert exc_chain[-1] == exc_details[0]
-            exc_chain[-1] = None
-            return True
-
-        try:
-            with ExitStack() as stack:
-                stack.callback(raise_exc, IndexError)
-                stack.callback(raise_exc, KeyError)
-                stack.callback(raise_exc, AttributeError)
-                stack.push(suppress_exc)
-                stack.callback(raise_exc, ValueError)
-                1 / 0
-        except IndexError as exc:
-            # self.assertIsInstance(exc.__context__, KeyError)
-            # self.assertIsInstance(exc.__context__.__context__, AttributeError)
-            # Inner exceptions were suppressed
-            # self.assertIsNone(exc.__context__.__context__.__context__)
-            assert tuple(exc_chain) == (ZeroDivisionError, None, AttributeError, KeyError, IndexError)
-        else:
-            self.fail("Expected IndexError, but no exception was raised")
-        # Check the inner exceptions
-        inner_exc = saved_details[1]
-        self.assertIsInstance(inner_exc, ValueError)
-        # self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
-
-    def test_exit_exception_non_suppressing(self):
-        # http://bugs.python.org/issue19092
-        def raise_exc(exc):
-            raise exc
-
-        def suppress_exc(*exc_details):
-            return True
-
-        try:
-            with ExitStack() as stack:
-                stack.callback(lambda: None)
-                stack.callback(raise_exc, IndexError)
-        except Exception as exc:
-            self.assertIsInstance(exc, IndexError)
-        else:
-            self.fail("Expected IndexError, but no exception was raised")
-
-        try:
-            with ExitStack() as stack:
-                stack.callback(raise_exc, KeyError)
-                stack.push(suppress_exc)
-                stack.callback(raise_exc, IndexError)
-        except Exception as exc:
-            self.assertIsInstance(exc, KeyError)
-        else:
-            self.fail("Expected KeyError, but no exception was raised")
-
-    def _test_exit_exception_with_correct_context(self):
-        # http://bugs.python.org/issue20317
-        @contextmanager
-        def gets_the_context_right(exc):
-            try:
-                yield
-            finally:
-                raise exc
-
-        exc1 = Exception(1)
-        exc2 = Exception(2)
-        exc3 = Exception(3)
-        exc4 = Exception(4)
-
-        # The contextmanager already fixes the context, so prior to the
-        # fix, ExitStack would try to fix it *again* and get into an
-        # infinite self-referential loop
-        try:
-            with ExitStack() as stack:
-                stack.enter_context(gets_the_context_right(exc4))
-                stack.enter_context(gets_the_context_right(exc3))
-                stack.enter_context(gets_the_context_right(exc2))
-                raise exc1
-        except Exception as exc:
-            self.assertIs(exc, exc4)
-            self.assertIs(exc.__context__, exc3)
-            self.assertIs(exc.__context__.__context__, exc2)
-            self.assertIs(exc.__context__.__context__.__context__, exc1)
-            self.assertIsNone(
-                       exc.__context__.__context__.__context__.__context__)
-
-    def _test_exit_exception_with_existing_context(self):
-        # Addresses a lack of test coverage discovered after checking in a
-        # fix for issue 20317 that still contained debugging code.
-        def raise_nested(inner_exc, outer_exc):
-            try:
-                raise inner_exc
-            finally:
-                raise outer_exc
-        exc1 = Exception(1)
-        exc2 = Exception(2)
-        exc3 = Exception(3)
-        exc4 = Exception(4)
-        exc5 = Exception(5)
-        try:
-            with ExitStack() as stack:
-                stack.callback(raise_nested, exc4, exc5)
-                stack.callback(raise_nested, exc2, exc3)
-                raise exc1
-        except Exception as exc:
-            self.assertIs(exc, exc5)
-            self.assertIs(exc.__context__, exc4)
-            self.assertIs(exc.__context__.__context__, exc3)
-            self.assertIs(exc.__context__.__context__.__context__, exc2)
-            self.assertIs(
-                 exc.__context__.__context__.__context__.__context__, exc1)
-            self.assertIsNone(
-                exc.__context__.__context__.__context__.__context__.__context__)
-
-
-
-    def test_body_exception_suppress(self):
-        def suppress_exc(*exc_details):
-            return True
-        try:
-            with ExitStack() as stack:
-                stack.push(suppress_exc)
-                1/0
-        except IndexError as exc:
-            self.fail("Expected no exception, got IndexError")
-
-    def test_exit_exception_chaining_suppress(self):
-        with ExitStack() as stack:
-            stack.push(lambda *exc: True)
-            stack.push(lambda *exc: 1/0)
-            stack.push(lambda *exc: {}[1])
-
-    def test_excessive_nesting(self):
-        # The original implementation would die with RecursionError here
-        with ExitStack() as stack:
-            for i in range(10000):
-                stack.callback(int)
-
-    def test_instance_bypass(self):
-        class Example(object): pass
-        cm = Example()
-        cm.__exit__ = object()
-        stack = ExitStack()
-        self.assertRaises(AttributeError, stack.enter_context, cm)
-        stack.push(cm)
-        self.assertIs(tuple(stack._exit_callbacks)[-1], cm)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/copy/setup.py b/copy/setup.py
deleted file mode 100644
index 43c07ce50..000000000
--- a/copy/setup.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise distutils will peek up our
-# copy module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-setup(name='micropython-copy',
-      version='0.0.2',
-      description='CPython copy module ported to MicroPython',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      install_requires=['micropython-types'],
-      py_modules=['copy'])
diff --git a/cpython-uasyncio/example_yield_coro.py b/cpython-uasyncio/example_yield_coro.py
deleted file mode 100644
index 2291162a4..000000000
--- a/cpython-uasyncio/example_yield_coro.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import uasyncio as asyncio
-
-
-def run1():
-    for i in range(1):
-        print('Hello World')
-        yield from asyncio.sleep(2)
-    print("run1 finished")
-
-def run2():
-    for i in range(3):
-        print('bar')
-        yield run1()
-        yield from asyncio.sleep(1)
-
-
-import logging
-logging.basicConfig(level=logging.INFO)
-loop = asyncio.get_event_loop()
-loop.create_task(run2())
-loop.run_forever()
diff --git a/cpython-uasyncio/metadata.txt b/cpython-uasyncio/metadata.txt
deleted file mode 100644
index 01093927b..000000000
--- a/cpython-uasyncio/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = cpython-backport
-type = module
-version = 0.2
diff --git a/cpython-uasyncio/patch.diff b/cpython-uasyncio/patch.diff
deleted file mode 100644
index be874237f..000000000
--- a/cpython-uasyncio/patch.diff
+++ /dev/null
@@ -1,27 +0,0 @@
-This patch shows changes done to asyncio.tasks.Task._step() from CPython 3.4.2.
-
---- tasks.py	2015-01-01 10:51:40.707114866 +0200
-+++ uasyncio.py	2015-01-01 10:54:20.172402890 +0200
-@@ -46,13 +55,16 @@
-                 # Bare yield relinquishes control for one event loop iteration.
-                 self._loop.call_soon(self._step)
-             elif inspect.isgenerator(result):
-+                #print("Scheduling", result)
-+                self._loop.create_task(result)
-+                self._loop.call_soon(self._step)
-                 # Yielding a generator is just wrong.
--                self._loop.call_soon(
--                    self._step, None,
--                    RuntimeError(
--                        'yield was used instead of yield from for '
--                        'generator in task {!r} with {}'.format(
--                            self, result)))
-+#                self._loop.call_soon(
-+#                    self._step, None,
-+#                    RuntimeError(
-+#                        'yield was used instead of yield from for '
-+#                        'generator in task {!r} with {}'.format(
-+#                            self, result)))
-             else:
-                 # Yielding something else is an error.
-                 self._loop.call_soon(
diff --git a/cpython-uasyncio/setup.py b/cpython-uasyncio/setup.py
deleted file mode 100644
index a70a33274..000000000
--- a/cpython-uasyncio/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-cpython-uasyncio',
-      version='0.2',
-      description='MicroPython module uasyncio ported to CPython',
-      long_description='This is MicroPython compatibility module, allowing applications using\nMicroPython-specific features to run on CPython.\n',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['uasyncio'])
diff --git a/cpython-uasyncio/uasyncio.py b/cpython-uasyncio/uasyncio.py
deleted file mode 100644
index fc83456f7..000000000
--- a/cpython-uasyncio/uasyncio.py
+++ /dev/null
@@ -1,99 +0,0 @@
-import inspect
-import asyncio
-import asyncio.futures as futures
-from asyncio import *
-
-
-OrgTask = Task
-
-class Task(OrgTask):
-
-    def _step(self, value=None, exc=None):
-        assert not self.done(), \
-            '_step(): already done: {!r}, {!r}, {!r}'.format(self, value, exc)
-        if self._must_cancel:
-            if not isinstance(exc, futures.CancelledError):
-                exc = futures.CancelledError()
-            self._must_cancel = False
-        coro = self._coro
-        self._fut_waiter = None
-
-        self.__class__._current_tasks[self._loop] = self
-        # Call either coro.throw(exc) or coro.send(value).
-        try:
-            if exc is not None:
-                result = coro.throw(exc)
-            elif value is not None:
-                result = coro.send(value)
-            else:
-                result = next(coro)
-        except StopIteration as exc:
-            self.set_result(exc.value)
-        except futures.CancelledError as exc:
-            super().cancel()  # I.e., Future.cancel(self).
-        except Exception as exc:
-            self.set_exception(exc)
-        except BaseException as exc:
-            self.set_exception(exc)
-            raise
-        else:
-            if isinstance(result, futures.Future):
-                # Yielded Future must come from Future.__iter__().
-                if result._blocking:
-                    result._blocking = False
-                    result.add_done_callback(self._wakeup)
-                    self._fut_waiter = result
-                    if self._must_cancel:
-                        if self._fut_waiter.cancel():
-                            self._must_cancel = False
-                else:
-                    self._loop.call_soon(
-                        self._step, None,
-                        RuntimeError(
-                            'yield was used instead of yield from '
-                            'in task {!r} with {!r}'.format(self, result)))
-            elif result is None:
-                # Bare yield relinquishes control for one event loop iteration.
-                self._loop.call_soon(self._step)
-            elif inspect.isgenerator(result):
-                #print("Scheduling", result)
-                self._loop.create_task(result)
-                self._loop.call_soon(self._step)
-                # Yielding a generator is just wrong.
-#                self._loop.call_soon(
-#                    self._step, None,
-#                    RuntimeError(
-#                        'yield was used instead of yield from for '
-#                        'generator in task {!r} with {}'.format(
-#                            self, result)))
-            else:
-                # Yielding something else is an error.
-                self._loop.call_soon(
-                    self._step, None,
-                    RuntimeError(
-                        'Task got bad yield: {!r}'.format(result)))
-        finally:
-            self.__class__._current_tasks.pop(self._loop)
-            self = None  # Needed to break cycles when an exception occurs.
-
-
-asyncio.tasks.Task = Task
-
-
-OrgStreamWriter = StreamWriter
-
-class StreamWriter(OrgStreamWriter):
-
-    def awrite(self, data):
-        if isinstance(data, str):
-            data = data.encode("utf-8")
-        self.write(data)
-        yield from self.drain()
-
-    def aclose(self):
-        self.close()
-        return
-        yield
-
-
-asyncio.streams.StreamWriter = StreamWriter
diff --git a/csv/csv.py b/csv/csv.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/csv/metadata.txt b/csv/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/csv/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/csv/setup.py b/csv/setup.py
deleted file mode 100644
index d59c0563c..000000000
--- a/csv/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-csv',
-      version='0.0.0',
-      description='Dummy csv module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['csv'])
diff --git a/curses.ascii/curses/ascii.py b/curses.ascii/curses/ascii.py
deleted file mode 100644
index 800fd8b4b..000000000
--- a/curses.ascii/curses/ascii.py
+++ /dev/null
@@ -1,99 +0,0 @@
-"""Constants and membership tests for ASCII characters"""
-
-NUL     = 0x00  # ^@
-SOH     = 0x01  # ^A
-STX     = 0x02  # ^B
-ETX     = 0x03  # ^C
-EOT     = 0x04  # ^D
-ENQ     = 0x05  # ^E
-ACK     = 0x06  # ^F
-BEL     = 0x07  # ^G
-BS      = 0x08  # ^H
-TAB     = 0x09  # ^I
-HT      = 0x09  # ^I
-LF      = 0x0a  # ^J
-NL      = 0x0a  # ^J
-VT      = 0x0b  # ^K
-FF      = 0x0c  # ^L
-CR      = 0x0d  # ^M
-SO      = 0x0e  # ^N
-SI      = 0x0f  # ^O
-DLE     = 0x10  # ^P
-DC1     = 0x11  # ^Q
-DC2     = 0x12  # ^R
-DC3     = 0x13  # ^S
-DC4     = 0x14  # ^T
-NAK     = 0x15  # ^U
-SYN     = 0x16  # ^V
-ETB     = 0x17  # ^W
-CAN     = 0x18  # ^X
-EM      = 0x19  # ^Y
-SUB     = 0x1a  # ^Z
-ESC     = 0x1b  # ^[
-FS      = 0x1c  # ^\
-GS      = 0x1d  # ^]
-RS      = 0x1e  # ^^
-US      = 0x1f  # ^_
-SP      = 0x20  # space
-DEL     = 0x7f  # delete
-
-controlnames = [
-"NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL",
-"BS",  "HT",  "LF",  "VT",  "FF",  "CR",  "SO",  "SI",
-"DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB",
-"CAN", "EM",  "SUB", "ESC", "FS",  "GS",  "RS",  "US",
-"SP"
-]
-
-def _ctoi(c):
-    if type(c) == type(""):
-        return ord(c)
-    else:
-        return c
-
-def isalnum(c): return isalpha(c) or isdigit(c)
-def isalpha(c): return isupper(c) or islower(c)
-def isascii(c): return _ctoi(c) <= 127          # ?
-def isblank(c): return _ctoi(c) in (8,32)
-def iscntrl(c): return _ctoi(c) <= 31
-def isdigit(c): return _ctoi(c) >= 48 and _ctoi(c) <= 57
-def isgraph(c): return _ctoi(c) >= 33 and _ctoi(c) <= 126
-def islower(c): return _ctoi(c) >= 97 and _ctoi(c) <= 122
-def isprint(c): return _ctoi(c) >= 32 and _ctoi(c) <= 126
-def ispunct(c): return _ctoi(c) != 32 and not isalnum(c)
-def isspace(c): return _ctoi(c) in (9, 10, 11, 12, 13, 32)
-def isupper(c): return _ctoi(c) >= 65 and _ctoi(c) <= 90
-def isxdigit(c): return isdigit(c) or \
-    (_ctoi(c) >= 65 and _ctoi(c) <= 70) or (_ctoi(c) >= 97 and _ctoi(c) <= 102)
-def isctrl(c): return _ctoi(c) < 32
-def ismeta(c): return _ctoi(c) > 127
-
-def ascii(c):
-    if type(c) == type(""):
-        return chr(_ctoi(c) & 0x7f)
-    else:
-        return _ctoi(c) & 0x7f
-
-def ctrl(c):
-    if type(c) == type(""):
-        return chr(_ctoi(c) & 0x1f)
-    else:
-        return _ctoi(c) & 0x1f
-
-def alt(c):
-    if type(c) == type(""):
-        return chr(_ctoi(c) | 0x80)
-    else:
-        return _ctoi(c) | 0x80
-
-def unctrl(c):
-    bits = _ctoi(c)
-    if bits == 0x7f:
-        rep = "^?"
-    elif isprint(bits & 0x7f):
-        rep = chr(bits & 0x7f)
-    else:
-        rep = "^" + chr(((bits & 0x7f) | 0x20) + 0x20)
-    if bits & 0x80:
-        return "!" + rep
-    return rep
diff --git a/curses.ascii/metadata.txt b/curses.ascii/metadata.txt
deleted file mode 100644
index d3c88d385..000000000
--- a/curses.ascii/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = cpython
-type = package
-version = 3.4.2
diff --git a/curses.ascii/setup.py b/curses.ascii/setup.py
deleted file mode 100644
index ac9d7d4e1..000000000
--- a/curses.ascii/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-curses.ascii',
-      version='3.4.2',
-      description='CPython curses.ascii module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['curses'])
diff --git a/datetime/datetime.py b/datetime/datetime.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/datetime/metadata.txt b/datetime/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/datetime/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/datetime/setup.py b/datetime/setup.py
deleted file mode 100644
index ca6f144d3..000000000
--- a/datetime/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-datetime',
-      version='0.0.1',
-      description='Dummy datetime module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['datetime'])
diff --git a/dbm/dbm.py b/dbm/dbm.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/dbm/metadata.txt b/dbm/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/dbm/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/dbm/setup.py b/dbm/setup.py
deleted file mode 100644
index e93f25ca5..000000000
--- a/dbm/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-dbm',
-      version='0.0.1',
-      description='Dummy dbm module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['dbm'])
diff --git a/decimal/decimal.py b/decimal/decimal.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/decimal/metadata.txt b/decimal/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/decimal/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/decimal/setup.py b/decimal/setup.py
deleted file mode 100644
index 779cdd603..000000000
--- a/decimal/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-decimal',
-      version='0.0.1',
-      description='Dummy decimal module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['decimal'])
diff --git a/difflib/difflib.py b/difflib/difflib.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/difflib/metadata.txt b/difflib/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/difflib/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/difflib/setup.py b/difflib/setup.py
deleted file mode 100644
index c26c11027..000000000
--- a/difflib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-difflib',
-      version='0.0.1',
-      description='Dummy difflib module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['difflib'])
diff --git a/email.charset/metadata.txt b/email.charset/metadata.txt
deleted file mode 100644
index 93f14bff7..000000000
--- a/email.charset/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5
-depends = functools, email.encoders, email.errors
diff --git a/email.charset/setup.py b/email.charset/setup.py
deleted file mode 100644
index 0208c6386..000000000
--- a/email.charset/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-email.charset',
-      version='0.5',
-      description='CPython email.charset module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['email'],
-      install_requires=['micropython-functools', 'micropython-email.encoders', 'micropython-email.errors'])
diff --git a/email.encoders/email/encoders.py b/email.encoders/email/encoders.py
deleted file mode 100644
index f9657f0a2..000000000
--- a/email.encoders/email/encoders.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright (C) 2001-2006 Python Software Foundation
-# Author: Barry Warsaw
-# Contact: email-sig@python.org
-
-"""Encodings and related functions."""
-
-__all__ = [
-    'encode_7or8bit',
-    'encode_base64',
-    'encode_noop',
-    'encode_quopri',
-    ]
-
-
-from base64 import encodebytes as _bencode
-from quopri import encodestring as _encodestring
-
-
-
-def _qencode(s):
-    enc = _encodestring(s, quotetabs=True)
-    # Must encode spaces, which quopri.encodestring() doesn't do
-    return enc.replace(b' ', b'=20')
-
-
-def encode_base64(msg):
-    """Encode the message's payload in Base64.
-
-    Also, add an appropriate Content-Transfer-Encoding header.
-    """
-    orig = msg.get_payload(decode=True)
-    encdata = str(_bencode(orig), 'ascii')
-    msg.set_payload(encdata)
-    msg['Content-Transfer-Encoding'] = 'base64'
-
-
-
-def encode_quopri(msg):
-    """Encode the message's payload in quoted-printable.
-
-    Also, add an appropriate Content-Transfer-Encoding header.
-    """
-    orig = msg.get_payload(decode=True)
-    encdata = _qencode(orig)
-    msg.set_payload(encdata)
-    msg['Content-Transfer-Encoding'] = 'quoted-printable'
-
-
-
-def encode_7or8bit(msg):
-    """Set the Content-Transfer-Encoding header to 7bit or 8bit."""
-    orig = msg.get_payload(decode=True)
-    if orig is None:
-        # There's no payload.  For backwards compatibility we use 7bit
-        msg['Content-Transfer-Encoding'] = '7bit'
-        return
-    # We play a trick to make this go fast.  If encoding/decode to ASCII
-    # succeeds, we know the data must be 7bit, otherwise treat it as 8bit.
-    try:
-        if isinstance(orig, str):
-            orig.encode('ascii')
-        else:
-            orig.decode('ascii')
-    except UnicodeError:
-        charset = msg.get_charset()
-        output_cset = charset and charset.output_charset
-        # iso-2022-* is non-ASCII but encodes to a 7-bit representation
-        if output_cset and output_cset.lower().startswith('iso-2022-'):
-            msg['Content-Transfer-Encoding'] = '7bit'
-        else:
-            msg['Content-Transfer-Encoding'] = '8bit'
-    else:
-        msg['Content-Transfer-Encoding'] = '7bit'
-
-
-
-def encode_noop(msg):
-    """Do nothing."""
diff --git a/email.encoders/metadata.txt b/email.encoders/metadata.txt
deleted file mode 100644
index d2883f45d..000000000
--- a/email.encoders/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5
-depends = base64, binascii, quopri, re-pcre, string
diff --git a/email.encoders/setup.py b/email.encoders/setup.py
deleted file mode 100644
index 65bd878bd..000000000
--- a/email.encoders/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-email.encoders',
-      version='0.5',
-      description='CPython email.encoders module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['email'],
-      install_requires=['micropython-base64', 'micropython-binascii', 'micropython-quopri', 'micropython-re-pcre', 'micropython-string'])
diff --git a/email.errors/metadata.txt b/email.errors/metadata.txt
deleted file mode 100644
index a468a34fd..000000000
--- a/email.errors/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5
diff --git a/email.errors/setup.py b/email.errors/setup.py
deleted file mode 100644
index d49d6c1d2..000000000
--- a/email.errors/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-email.errors',
-      version='0.5',
-      description='CPython email.errors module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['email'])
diff --git a/email.feedparser/metadata.txt b/email.feedparser/metadata.txt
deleted file mode 100644
index 1d1a5d43f..000000000
--- a/email.feedparser/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5
-depends = re-pcre, email.errors, email.message, email.internal
diff --git a/email.feedparser/setup.py b/email.feedparser/setup.py
deleted file mode 100644
index fbf36bb2d..000000000
--- a/email.feedparser/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-email.feedparser',
-      version='0.5',
-      description='CPython email.feedparser module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['email'],
-      install_requires=['micropython-re-pcre', 'micropython-email.errors', 'micropython-email.message', 'micropython-email.internal'])
diff --git a/email.header/metadata.txt b/email.header/metadata.txt
deleted file mode 100644
index 045e08bdf..000000000
--- a/email.header/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5.1
-depends = re-pcre, binascii, email.encoders, email.errors, email.charset
diff --git a/email.header/setup.py b/email.header/setup.py
deleted file mode 100644
index 225ea20b6..000000000
--- a/email.header/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-email.header',
-      version='0.5.1',
-      description='CPython email.header module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['email'],
-      install_requires=['micropython-re-pcre', 'micropython-binascii', 'micropython-email.encoders', 'micropython-email.errors', 'micropython-email.charset'])
diff --git a/email.internal/metadata.txt b/email.internal/metadata.txt
deleted file mode 100644
index 9d7d38dc8..000000000
--- a/email.internal/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5
-depends = re-pcre, base64, binascii, functools, string, calendar, abc, email.errors, email.header, email.charset, email.utils
diff --git a/email.internal/setup.py b/email.internal/setup.py
deleted file mode 100644
index ea9f8c0c2..000000000
--- a/email.internal/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-email.internal',
-      version='0.5',
-      description='CPython email.internal module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['email'],
-      install_requires=['micropython-re-pcre', 'micropython-base64', 'micropython-binascii', 'micropython-functools', 'micropython-string', 'micropython-calendar', 'micropython-abc', 'micropython-email.errors', 'micropython-email.header', 'micropython-email.charset', 'micropython-email.utils'])
diff --git a/email.message/metadata.txt b/email.message/metadata.txt
deleted file mode 100644
index 510d44ea0..000000000
--- a/email.message/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5.2
-depends = re-pcre, uu, base64, binascii, email.utils, email.errors, email.charset
diff --git a/email.message/setup.py b/email.message/setup.py
deleted file mode 100644
index 90ccfa83d..000000000
--- a/email.message/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-email.message',
-      version='0.5.2',
-      description='CPython email.message module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['email'],
-      install_requires=['micropython-re-pcre', 'micropython-uu', 'micropython-base64', 'micropython-binascii', 'micropython-email.utils', 'micropython-email.errors', 'micropython-email.charset'])
diff --git a/email.parser/email/parser.py b/email.parser/email/parser.py
deleted file mode 100644
index 34ee58acf..000000000
--- a/email.parser/email/parser.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# Copyright (C) 2001-2007 Python Software Foundation
-# Author: Barry Warsaw, Thomas Wouters, Anthony Baxter
-# Contact: email-sig@python.org
-
-"""A parser of RFC 2822 and MIME email messages."""
-
-__all__ = ['Parser', 'HeaderParser', 'BytesParser', 'BytesHeaderParser']
-
-import warnings
-from io import StringIO, TextIOWrapper
-
-from email.feedparser import FeedParser, BytesFeedParser
-from email.message import Message
-from email._policybase import compat32
-
-
-
-class Parser:
-    def __init__(self, _class=Message, policy=compat32):
-        """Parser of RFC 2822 and MIME email messages.
-
-        Creates an in-memory object tree representing the email message, which
-        can then be manipulated and turned over to a Generator to return the
-        textual representation of the message.
-
-        The string must be formatted as a block of RFC 2822 headers and header
-        continuation lines, optionally preceeded by a `Unix-from' header.  The
-        header block is terminated either by the end of the string or by a
-        blank line.
-
-        _class is the class to instantiate for new message objects when they
-        must be created.  This class must have a constructor that can take
-        zero arguments.  Default is Message.Message.
-
-        The policy keyword specifies a policy object that controls a number of
-        aspects of the parser's operation.  The default policy maintains
-        backward compatibility.
-
-        """
-        self._class = _class
-        self.policy = policy
-
-    def parse(self, fp, headersonly=False):
-        """Create a message structure from the data in a file.
-
-        Reads all the data from the file and returns the root of the message
-        structure.  Optional headersonly is a flag specifying whether to stop
-        parsing after reading the headers or not.  The default is False,
-        meaning it parses the entire contents of the file.
-        """
-        feedparser = FeedParser(self._class, policy=self.policy)
-        if headersonly:
-            feedparser._set_headersonly()
-        while True:
-            data = fp.read(8192)
-            if not data:
-                break
-            feedparser.feed(data)
-        return feedparser.close()
-
-    def parsestr(self, text, headersonly=False):
-        """Create a message structure from a string.
-
-        Returns the root of the message structure.  Optional headersonly is a
-        flag specifying whether to stop parsing after reading the headers or
-        not.  The default is False, meaning it parses the entire contents of
-        the file.
-        """
-        return self.parse(StringIO(text), headersonly=headersonly)
-
-
-
-class HeaderParser(Parser):
-    def parse(self, fp, headersonly=True):
-        return Parser.parse(self, fp, True)
-
-    def parsestr(self, text, headersonly=True):
-        return Parser.parsestr(self, text, True)
-
-
-class BytesParser:
-
-    def __init__(self, *args, **kw):
-        """Parser of binary RFC 2822 and MIME email messages.
-
-        Creates an in-memory object tree representing the email message, which
-        can then be manipulated and turned over to a Generator to return the
-        textual representation of the message.
-
-        The input must be formatted as a block of RFC 2822 headers and header
-        continuation lines, optionally preceeded by a `Unix-from' header.  The
-        header block is terminated either by the end of the input or by a
-        blank line.
-
-        _class is the class to instantiate for new message objects when they
-        must be created.  This class must have a constructor that can take
-        zero arguments.  Default is Message.Message.
-        """
-        self.parser = Parser(*args, **kw)
-
-    def parse(self, fp, headersonly=False):
-        """Create a message structure from the data in a binary file.
-
-        Reads all the data from the file and returns the root of the message
-        structure.  Optional headersonly is a flag specifying whether to stop
-        parsing after reading the headers or not.  The default is False,
-        meaning it parses the entire contents of the file.
-        """
-        fp = TextIOWrapper(fp, encoding='ascii', errors='surrogateescape')
-        with fp:
-            return self.parser.parse(fp, headersonly)
-
-
-    def parsebytes(self, text, headersonly=False):
-        """Create a message structure from a byte string.
-
-        Returns the root of the message structure.  Optional headersonly is a
-        flag specifying whether to stop parsing after reading the headers or
-        not.  The default is False, meaning it parses the entire contents of
-        the file.
-        """
-        text = text.decode('ASCII', errors='surrogateescape')
-        return self.parser.parsestr(text, headersonly)
-
-
-class BytesHeaderParser(BytesParser):
-    def parse(self, fp, headersonly=True):
-        return BytesParser.parse(self, fp, headersonly=True)
-
-    def parsebytes(self, text, headersonly=True):
-        return BytesParser.parsebytes(self, text, headersonly=True)
diff --git a/email.parser/metadata.txt b/email.parser/metadata.txt
deleted file mode 100644
index f014645d8..000000000
--- a/email.parser/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5
-depends = warnings, email.feedparser, email.message, email.internal
diff --git a/email.parser/setup.py b/email.parser/setup.py
deleted file mode 100644
index afadd466b..000000000
--- a/email.parser/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-email.parser',
-      version='0.5',
-      description='CPython email.parser module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['email'],
-      install_requires=['micropython-warnings', 'micropython-email.feedparser', 'micropython-email.message', 'micropython-email.internal'])
diff --git a/email.utils/metadata.txt b/email.utils/metadata.txt
deleted file mode 100644
index ae240e162..000000000
--- a/email.utils/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5
-depends = os, re-pcre, base64, random, datetime, urlib.parse, warnings, quopri, email.charset
diff --git a/email.utils/setup.py b/email.utils/setup.py
deleted file mode 100644
index 87d33f563..000000000
--- a/email.utils/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-email.utils',
-      version='0.5',
-      description='CPython email.utils module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['email'],
-      install_requires=['micropython-os', 'micropython-re-pcre', 'micropython-base64', 'micropython-random', 'micropython-datetime', 'micropython-urlib.parse', 'micropython-warnings', 'micropython-quopri', 'micropython-email.charset'])
diff --git a/errno/errno.py b/errno/errno.py
deleted file mode 100644
index 7b7935ef8..000000000
--- a/errno/errno.py
+++ /dev/null
@@ -1,38 +0,0 @@
-EPERM   = 1   # Operation not permitted
-ENOENT  = 2   # No such file or directory
-ESRCH   = 3   # No such process
-EINTR   = 4   # Interrupted system call
-EIO     = 5   # I/O error
-ENXIO   = 6   # No such device or address
-E2BIG   = 7   # Argument list too long
-ENOEXEC = 8   # Exec format error
-EBADF   = 9   # Bad file number
-ECHILD  = 10  # No child processes
-EAGAIN  = 11  # Try again
-ENOMEM  = 12  # Out of memory
-EACCES  = 13  # Permission denied
-EFAULT  = 14  # Bad address
-ENOTBLK = 15  # Block device required
-EBUSY   = 16  # Device or resource busy
-EEXIST  = 17  # File exists
-EXDEV   = 18  # Cross-device link
-ENODEV  = 19  # No such device
-ENOTDIR = 20  # Not a directory
-EISDIR  = 21  # Is a directory
-EINVAL  = 22  # Invalid argument
-ENFILE  = 23  # File table overflow
-EMFILE  = 24  # Too many open files
-ENOTTY  = 25  # Not a typewriter
-ETXTBSY = 26  # Text file busy
-EFBIG   = 27  # File too large
-ENOSPC  = 28  # No space left on device
-ESPIPE  = 29  # Illegal seek
-EROFS   = 30  # Read-only file system
-EMLINK  = 31  # Too many links
-EPIPE   = 32  # Broken pipe
-EDOM    = 33  # Math argument out of domain of func
-ERANGE  = 34  # Math result not representable
-EAFNOSUPPORT = 97 # Address family not supported by protocol
-ECONNRESET = 104 # Connection timed out
-ETIMEDOUT = 110 # Connection timed out
-EINPROGRESS = 115 # Operation now in progress
diff --git a/errno/metadata.txt b/errno/metadata.txt
deleted file mode 100644
index c14869284..000000000
--- a/errno/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1.3
diff --git a/errno/setup.py b/errno/setup.py
deleted file mode 100644
index 78b1c1baf..000000000
--- a/errno/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-errno',
-      version='0.1.3',
-      description='errno module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['errno'])
diff --git a/fcntl/metadata.txt b/fcntl/metadata.txt
deleted file mode 100644
index 3e79861b1..000000000
--- a/fcntl/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.0.3
-author = Paul Sokolovsky
-depends = ffilib
diff --git a/fcntl/setup.py b/fcntl/setup.py
deleted file mode 100644
index e53593475..000000000
--- a/fcntl/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-fcntl',
-      version='0.0.3',
-      description='fcntl module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['fcntl'],
-      install_requires=['micropython-ffilib'])
diff --git a/ffilib/metadata.txt b/ffilib/metadata.txt
deleted file mode 100644
index c3fcc107e..000000000
--- a/ffilib/metadata.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-dist_name = ffilib
-srctype = micropython-lib
-type = module
-version = 0.1.2
-author = Damien George
-desc = MicroPython FFI helper module
-long_desc = MicroPython FFI helper module to easily interface with underlying shared libraries
diff --git a/ffilib/setup.py b/ffilib/setup.py
deleted file mode 100644
index 513410dd3..000000000
--- a/ffilib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-ffilib',
-      version='0.1.2',
-      description='MicroPython FFI helper module',
-      long_description='MicroPython FFI helper module to easily interface with underlying shared libraries',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Damien George',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['ffilib'])
diff --git a/fnmatch/fnmatch.py b/fnmatch/fnmatch.py
deleted file mode 100644
index d5f7a43f2..000000000
--- a/fnmatch/fnmatch.py
+++ /dev/null
@@ -1,111 +0,0 @@
-"""Filename matching with shell patterns.
-
-fnmatch(FILENAME, PATTERN) matches according to the local convention.
-fnmatchcase(FILENAME, PATTERN) always takes case in account.
-
-The functions operate by translating the pattern into a regular
-expression.  They cache the compiled regular expressions for speed.
-
-The function translate(PATTERN) returns a regular expression
-corresponding to PATTERN.  (It does not compile it.)
-"""
-import os
-import os.path
-import posixpath
-import re
-#import functools
-
-__all__ = ["filter", "fnmatch", "fnmatchcase", "translate"]
-
-def fnmatch(name, pat):
-    """Test whether FILENAME matches PATTERN.
-
-    Patterns are Unix shell style:
-
-    *       matches everything
-    ?       matches any single character
-    [seq]   matches any character in seq
-    [!seq]  matches any char not in seq
-
-    An initial period in FILENAME is not special.
-    Both FILENAME and PATTERN are first case-normalized
-    if the operating system requires it.
-    If you don't want this, use fnmatchcase(FILENAME, PATTERN).
-    """
-    name = os.path.normcase(name)
-    pat = os.path.normcase(pat)
-    return fnmatchcase(name, pat)
-
-#@functools.lru_cache(maxsize=256, typed=True)
-def _compile_pattern(pat):
-    if isinstance(pat, bytes):
-        pat_str = str(pat, 'ISO-8859-1')
-        res_str = translate(pat_str)
-        res = bytes(res_str, 'ISO-8859-1')
-    else:
-        res = translate(pat)
-    return re.compile(res).match
-
-def filter(names, pat):
-    """Return the subset of the list NAMES that match PAT."""
-    result = []
-    pat = os.path.normcase(pat)
-    match = _compile_pattern(pat)
-    if os.path is posixpath:
-        # normcase on posix is NOP. Optimize it away from the loop.
-        for name in names:
-            if match(name):
-                result.append(name)
-    else:
-        for name in names:
-            if match(os.path.normcase(name)):
-                result.append(name)
-    return result
-
-def fnmatchcase(name, pat):
-    """Test whether FILENAME matches PATTERN, including case.
-
-    This is a version of fnmatch() which doesn't case-normalize
-    its arguments.
-    """
-    match = _compile_pattern(pat)
-    return match(name) is not None
-
-
-def translate(pat):
-    """Translate a shell PATTERN to a regular expression.
-
-    There is no way to quote meta-characters.
-    """
-
-    i, n = 0, len(pat)
-    res = ''
-    while i < n:
-        c = pat[i]
-        i = i+1
-        if c == '*':
-            res = res + '.*'
-        elif c == '?':
-            res = res + '.'
-        elif c == '[':
-            j = i
-            if j < n and pat[j] == '!':
-                j = j+1
-            if j < n and pat[j] == ']':
-                j = j+1
-            while j < n and pat[j] != ']':
-                j = j+1
-            if j >= n:
-                res = res + '\\['
-            else:
-                stuff = pat[i:j].replace('\\','\\\\')
-                i = j+1
-                if stuff[0] == '!':
-                    stuff = '^' + stuff[1:]
-                elif stuff[0] == '^':
-                    stuff = '\\' + stuff
-                res = '%s[%s]' % (res, stuff)
-        else:
-            res = res + re.escape(c)
-    # Original patterns is undefined, see http://bugs.python.org/issue21464
-    return '(?ms)' + res + '\Z'
diff --git a/fnmatch/metadata.txt b/fnmatch/metadata.txt
deleted file mode 100644
index 17eb9c92f..000000000
--- a/fnmatch/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = module
-version = 0.5.1
-depends = os, os.path, posixpath, re-pcre
diff --git a/fnmatch/setup.py b/fnmatch/setup.py
deleted file mode 100644
index f14c9319c..000000000
--- a/fnmatch/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-fnmatch',
-      version='0.5.1',
-      description='CPython fnmatch module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['fnmatch'],
-      install_requires=['micropython-os', 'micropython-os.path', 'micropython-posixpath', 'micropython-re-pcre'])
diff --git a/fnmatch/test_fnmatch.py b/fnmatch/test_fnmatch.py
deleted file mode 100644
index 4d5e9d728..000000000
--- a/fnmatch/test_fnmatch.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""Test cases for the fnmatch module."""
-
-from test import support
-import unittest
-
-from fnmatch import fnmatch, fnmatchcase, translate, filter
-
-class FnmatchTestCase(unittest.TestCase):
-
-    def check_match(self, filename, pattern, should_match=1, fn=fnmatch):
-        if should_match:
-            self.assertTrue(fn(filename, pattern),
-                         "expected %r to match pattern %r"
-                         % (filename, pattern))
-        else:
-            self.assertTrue(not fn(filename, pattern),
-                         "expected %r not to match pattern %r"
-                         % (filename, pattern))
-
-    def test_fnmatch(self):
-        check = self.check_match
-        check('abc', 'abc')
-        check('abc', '?*?')
-        check('abc', '???*')
-        check('abc', '*???')
-        check('abc', '???')
-        check('abc', '*')
-        check('abc', 'ab[cd]')
-        check('abc', 'ab[!de]')
-        check('abc', 'ab[de]', 0)
-        check('a', '??', 0)
-        check('a', 'b', 0)
-
-        # these test that '\' is handled correctly in character sets;
-        # see SF bug #409651
-        check('\\', r'[\]')
-        check('a', r'[!\]')
-        check('\\', r'[!\]', 0)
-
-        # test that filenames with newlines in them are handled correctly.
-        # http://bugs.python.org/issue6665
-        check('foo\nbar', 'foo*')
-        check('foo\nbar\n', 'foo*')
-        check('\nfoo', 'foo*', False)
-        check('\n', '*')
-
-    def _test_mix_bytes_str(self):
-        self.assertRaises(TypeError, fnmatch, 'test', b'*')
-        self.assertRaises(TypeError, fnmatch, b'test', '*')
-        self.assertRaises(TypeError, fnmatchcase, 'test', b'*')
-        self.assertRaises(TypeError, fnmatchcase, b'test', '*')
-
-    def test_fnmatchcase(self):
-        check = self.check_match
-        check('AbC', 'abc', 0, fnmatchcase)
-        check('abc', 'AbC', 0, fnmatchcase)
-
-    def test_bytes(self):
-        self.check_match(b'test', b'te*')
-        self.check_match(b'test\xff', b'te*\xff')
-        self.check_match(b'foo\nbar', b'foo*')
-
-class TranslateTestCase(unittest.TestCase):
-
-    def test_translate(self):
-        self.assertEqual(translate('*'), '(?ms).*\Z')
-        self.assertEqual(translate('?'), '(?ms).\Z')
-        self.assertEqual(translate('a?b*'), '(?ms)a.b.*\Z')
-        self.assertEqual(translate('[abc]'), '(?ms)[abc]\Z')
-        self.assertEqual(translate('[]]'), '(?ms)[]]\Z')
-        self.assertEqual(translate('[!x]'), '(?ms)[^x]\Z')
-        self.assertEqual(translate('[^x]'), '(?ms)[\\^x]\Z')
-        self.assertEqual(translate('[x'), '(?ms)\\[x\Z')
-
-
-class FilterTestCase(unittest.TestCase):
-
-    def test_filter(self):
-        self.assertEqual(filter(['a', 'b'], 'a'), ['a'])
-
-
-def test_main():
-    support.run_unittest(FnmatchTestCase,
-                         TranslateTestCase,
-                         FilterTestCase)
-
-
-if __name__ == "__main__":
-    test_main()
diff --git a/formatter/formatter.py b/formatter/formatter.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/formatter/metadata.txt b/formatter/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/formatter/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/formatter/setup.py b/formatter/setup.py
deleted file mode 100644
index 77f245ffd..000000000
--- a/formatter/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-formatter',
-      version='0.0.0',
-      description='Dummy formatter module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['formatter'])
diff --git a/fractions/fractions.py b/fractions/fractions.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/fractions/metadata.txt b/fractions/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/fractions/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/fractions/setup.py b/fractions/setup.py
deleted file mode 100644
index 7f963e566..000000000
--- a/fractions/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-fractions',
-      version='0.0.0',
-      description='Dummy fractions module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['fractions'])
diff --git a/ftplib/ftplib.py b/ftplib/ftplib.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ftplib/metadata.txt b/ftplib/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/ftplib/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/ftplib/setup.py b/ftplib/setup.py
deleted file mode 100644
index 53b8ea2f6..000000000
--- a/ftplib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-ftplib',
-      version='0.0.0',
-      description='Dummy ftplib module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['ftplib'])
diff --git a/functools/functools.py b/functools/functools.py
deleted file mode 100644
index ae8c2cf0e..000000000
--- a/functools/functools.py
+++ /dev/null
@@ -1,26 +0,0 @@
-def partial(func, *args, **kwargs):
-    def _partial(*more_args, **more_kwargs):
-        kw = kwargs.copy()
-        kw.update(more_kwargs)
-        func(*(args + more_args), **kw)
-    return _partial
-
-
-def update_wrapper(wrapper, wrapped):
-    # Dummy impl
-    return wrapper
-
-
-def wraps(wrapped):
-    # Dummy impl
-    return lambda x: x
-
-def reduce(function, iterable, initializer=None):
-    it = iter(iterable)
-    if initializer is None:
-        value = next(it)
-    else:
-        value = initializer
-    for element in it:
-        value = function(value, element)
-    return value
diff --git a/functools/metadata.txt b/functools/metadata.txt
deleted file mode 100644
index 65d7b523d..000000000
--- a/functools/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.0.4
diff --git a/functools/setup.py b/functools/setup.py
deleted file mode 100644
index 97945d506..000000000
--- a/functools/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-functools',
-      version='0.0.4',
-      description='functools module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['functools'])
diff --git a/functools/test_reduce.py b/functools/test_reduce.py
deleted file mode 100644
index 30f38eeca..000000000
--- a/functools/test_reduce.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from functools import reduce
-
-res = reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
-assert(res == 1 + 2 + 3 + 4 + 5)
-
-res = reduce(lambda x, y: x + y, [1, 2, 3, 4, 5], 10)
-assert(res == 10 + 1 + 2 + 3 + 4 + 5)
diff --git a/getopt/metadata.txt b/getopt/metadata.txt
deleted file mode 100644
index 5ab11258c..000000000
--- a/getopt/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = module
-version = 0.5
-depends = os
diff --git a/getopt/setup.py b/getopt/setup.py
deleted file mode 100644
index e8cd70285..000000000
--- a/getopt/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-getopt',
-      version='0.5',
-      description='CPython getopt module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['getopt'],
-      install_requires=['micropython-os'])
diff --git a/getpass/getpass.py b/getpass/getpass.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/getpass/metadata.txt b/getpass/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/getpass/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/getpass/setup.py b/getpass/setup.py
deleted file mode 100644
index d0daf4b4a..000000000
--- a/getpass/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-getpass',
-      version='0.0.0',
-      description='Dummy getpass module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['getpass'])
diff --git a/gettext/gettext.py b/gettext/gettext.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/gettext/metadata.txt b/gettext/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/gettext/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/gettext/setup.py b/gettext/setup.py
deleted file mode 100644
index 912fe87d1..000000000
--- a/gettext/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-gettext',
-      version='0.0.0',
-      description='Dummy gettext module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['gettext'])
diff --git a/glob/metadata.txt b/glob/metadata.txt
deleted file mode 100644
index 0c5e5114f..000000000
--- a/glob/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = module
-version = 0.5.1
-depends = os, re-pcre, fnmatch
diff --git a/glob/setup.py b/glob/setup.py
deleted file mode 100644
index 553e93824..000000000
--- a/glob/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-glob',
-      version='0.5.1',
-      description='CPython glob module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['glob'],
-      install_requires=['micropython-os', 'micropython-re-pcre', 'micropython-fnmatch'])
diff --git a/glob/test_glob.py b/glob/test_glob.py
deleted file mode 100644
index 98b053e18..000000000
--- a/glob/test_glob.py
+++ /dev/null
@@ -1,179 +0,0 @@
-import glob
-import os
-import os.path
-import shutil
-import sys
-import unittest
-
-from test.support import (run_unittest, TESTFN, skip_unless_symlink,
-                          can_symlink, create_empty_file)
-
-
-class GlobTests(unittest.TestCase):
-
-    def norm(self, *parts):
-        return os.path.normpath(os.path.join(self.tempdir, *parts))
-
-    def mktemp(self, *parts):
-        filename = self.norm(*parts)
-        base, file = os.path.split(filename)
-        if not os.path.exists(base):
-            os.makedirs(base)
-        create_empty_file(filename)
-
-    def setUp(self):
-        self.tempdir = TESTFN + "_dir"
-        self.mktemp('a', 'D')
-        self.mktemp('aab', 'F')
-        self.mktemp('.aa', 'G')
-        self.mktemp('.bb', 'H')
-        self.mktemp('aaa', 'zzzF')
-        self.mktemp('ZZZ')
-        self.mktemp('a', 'bcd', 'EF')
-        self.mktemp('a', 'bcd', 'efg', 'ha')
-        if can_symlink():
-            os.symlink(self.norm('broken'), self.norm('sym1'))
-            os.symlink('broken', self.norm('sym2'))
-            os.symlink(os.path.join('a', 'bcd'), self.norm('sym3'))
-
-    def tearDown(self):
-        shutil.rmtree(self.tempdir)
-
-    def glob(self, *parts):
-        if len(parts) == 1:
-            pattern = parts[0]
-        else:
-            pattern = os.path.join(*parts)
-        p = os.path.join(self.tempdir, pattern)
-        res = glob.glob(p)
-        self.assertEqual(list(glob.iglob(p)), res)
-        bres = [os.fsencode(x) for x in res]
-        self.assertEqual(glob.glob(os.fsencode(p)), bres)
-        self.assertEqual(list(glob.iglob(os.fsencode(p))), bres)
-        return res
-
-    def assertSequencesEqual_noorder(self, l1, l2):
-        l1 = list(l1)
-        l2 = list(l2)
-        self.assertEqual(set(l1), set(l2))
-        self.assertEqual(sorted(l1), sorted(l2))
-
-    def test_glob_literal(self):
-        eq = self.assertSequencesEqual_noorder
-        eq(self.glob('a'), [self.norm('a')])
-        eq(self.glob('a', 'D'), [self.norm('a', 'D')])
-        eq(self.glob('aab'), [self.norm('aab')])
-        eq(self.glob('zymurgy'), [])
-
-        res = glob.glob('*')
-        self.assertEqual({type(r) for r in res}, {str})
-        res = glob.glob(os.path.join(os.curdir, '*'))
-        self.assertEqual({type(r) for r in res}, {str})
-
-#        res = glob.glob(b'*')
-#        self.assertEqual({type(r) for r in res}, {bytes})
-#        res = glob.glob(os.path.join(os.fsencode(os.curdir), b'*'))
-#        self.assertEqual({type(r) for r in res}, {bytes})
-
-    def test_glob_one_directory(self):
-        eq = self.assertSequencesEqual_noorder
-        eq(self.glob('a*'), map(self.norm, ['a', 'aab', 'aaa']))
-        eq(self.glob('*a'), map(self.norm, ['a', 'aaa']))
-        eq(self.glob('.*'), map(self.norm, ['.aa', '.bb']))
-        eq(self.glob('?aa'), map(self.norm, ['aaa']))
-        eq(self.glob('aa?'), map(self.norm, ['aaa', 'aab']))
-        eq(self.glob('aa[ab]'), map(self.norm, ['aaa', 'aab']))
-        eq(self.glob('*q'), [])
-
-    def test_glob_nested_directory(self):
-        eq = self.assertSequencesEqual_noorder
-        if os.path.normcase("abCD") == "abCD":
-            # case-sensitive filesystem
-            eq(self.glob('a', 'bcd', 'E*'), [self.norm('a', 'bcd', 'EF')])
-        else:
-            # case insensitive filesystem
-            eq(self.glob('a', 'bcd', 'E*'), [self.norm('a', 'bcd', 'EF'),
-                                             self.norm('a', 'bcd', 'efg')])
-        eq(self.glob('a', 'bcd', '*g'), [self.norm('a', 'bcd', 'efg')])
-
-    def test_glob_directory_names(self):
-        eq = self.assertSequencesEqual_noorder
-        eq(self.glob('*', 'D'), [self.norm('a', 'D')])
-        eq(self.glob('*', '*a'), [])
-        eq(self.glob('a', '*', '*', '*a'),
-           [self.norm('a', 'bcd', 'efg', 'ha')])
-        eq(self.glob('?a?', '*F'), [self.norm('aaa', 'zzzF'),
-                                    self.norm('aab', 'F')])
-
-    def test_glob_directory_with_trailing_slash(self):
-        # Patterns ending with a slash shouldn't match non-dirs
-        res = glob.glob(self.norm('Z*Z') + os.sep)
-        self.assertEqual(res, [])
-        res = glob.glob(self.norm('ZZZ') + os.sep)
-        self.assertEqual(res, [])
-        # When there is a wildcard pattern which ends with os.sep, glob()
-        # doesn't blow up.
-        res = glob.glob(self.norm('aa*') + os.sep)
-        self.assertEqual(len(res), 2)
-        # either of these results is reasonable
-        self.assertIn(set(res), [
-                      {self.norm('aaa'), self.norm('aab')},
-                      {self.norm('aaa') + os.sep, self.norm('aab') + os.sep},
-                      ])
-
-    def test_glob_bytes_directory_with_trailing_slash(self):
-        # Same as test_glob_directory_with_trailing_slash, but with a
-        # bytes argument.
-        res = glob.glob(os.fsencode(self.norm('Z*Z') + os.sep))
-        self.assertEqual(res, [])
-        res = glob.glob(os.fsencode(self.norm('ZZZ') + os.sep))
-        self.assertEqual(res, [])
-        res = glob.glob(os.fsencode(self.norm('aa*') + os.sep))
-        self.assertEqual(len(res), 2)
-        # either of these results is reasonable
-        self.assertIn(set(res), [
-                      {os.fsencode(self.norm('aaa')),
-                       os.fsencode(self.norm('aab'))},
-                      {os.fsencode(self.norm('aaa') + os.sep),
-                       os.fsencode(self.norm('aab') + os.sep)},
-                      ])
-
-    @skip_unless_symlink
-    def test_glob_symlinks(self):
-        eq = self.assertSequencesEqual_noorder
-        eq(self.glob('sym3'), [self.norm('sym3')])
-        eq(self.glob('sym3', '*'), [self.norm('sym3', 'EF'),
-                                    self.norm('sym3', 'efg')])
-        self.assertIn(self.glob('sym3' + os.sep),
-                      [[self.norm('sym3')], [self.norm('sym3') + os.sep]])
-        eq(self.glob('*', '*F'),
-           [self.norm('aaa', 'zzzF'),
-            self.norm('aab', 'F'), self.norm('sym3', 'EF')])
-
-    @skip_unless_symlink
-    def test_glob_broken_symlinks(self):
-        eq = self.assertSequencesEqual_noorder
-        eq(self.glob('sym*'), [self.norm('sym1'), self.norm('sym2'),
-                               self.norm('sym3')])
-        eq(self.glob('sym1'), [self.norm('sym1')])
-        eq(self.glob('sym2'), [self.norm('sym2')])
-
-#    @unittest.skipUnless(sys.platform == "win32", "Win32 specific test")
-    def _test_glob_magic_in_drive(self):
-        eq = self.assertSequencesEqual_noorder
-        eq(glob.glob('*:'), [])
-        eq(glob.glob(b'*:'), [])
-        eq(glob.glob('?:'), [])
-        eq(glob.glob(b'?:'), [])
-        eq(glob.glob('\\\\?\\c:\\'), ['\\\\?\\c:\\'])
-        eq(glob.glob(b'\\\\?\\c:\\'), [b'\\\\?\\c:\\'])
-        eq(glob.glob('\\\\*\\*\\'), [])
-        eq(glob.glob(b'\\\\*\\*\\'), [])
-
-
-def test_main():
-    run_unittest(GlobTests)
-
-
-if __name__ == "__main__":
-    test_main()
diff --git a/gzip/gzip.py b/gzip/gzip.py
deleted file mode 100644
index be4e8f4a5..000000000
--- a/gzip/gzip.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#import zlib
-import uzlib as zlib
-
-FTEXT    = 1
-FHCRC    = 2
-FEXTRA   = 4
-FNAME    = 8
-FCOMMENT = 16
-
-def decompress(data):
-    assert data[0] == 0x1f and data[1] == 0x8b
-    assert data[2] == 8
-    flg = data[3]
-    assert flg & 0xe0 == 0
-    i = 10
-    if flg & FEXTRA:
-        i += data[11] << 8 + data[10] + 2
-    if flg & FNAME:
-        while data[i]:
-            i += 1
-        i += 1
-    if flg & FCOMMENT:
-        while data[i]:
-            i += 1
-        i += 1
-    if flg & FHCRC:
-        i += 2
-    return zlib.decompress(memoryview(data)[i:], -15)
diff --git a/gzip/metadata.txt b/gzip/metadata.txt
deleted file mode 100644
index 6b5dc7328..000000000
--- a/gzip/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=micropython-lib
-type=module
-version=0.1
diff --git a/gzip/setup.py b/gzip/setup.py
deleted file mode 100644
index b2287005c..000000000
--- a/gzip/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-gzip',
-      version='0.1',
-      description='gzip module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['gzip'])
diff --git a/hashlib/hashlib/__init__.py b/hashlib/hashlib/__init__.py
deleted file mode 100644
index f71c490d7..000000000
--- a/hashlib/hashlib/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .sha256 import sha224, sha256
-from .sha512 import sha384, sha512
diff --git a/hashlib/hashlib/sha224.py b/hashlib/hashlib/sha224.py
deleted file mode 100644
index e413a1a5c..000000000
--- a/hashlib/hashlib/sha224.py
+++ /dev/null
@@ -1 +0,0 @@
-from .sha256 import sha224
diff --git a/hashlib/hashlib/sha256.py b/hashlib/hashlib/sha256.py
deleted file mode 100644
index 8c013b5fd..000000000
--- a/hashlib/hashlib/sha256.py
+++ /dev/null
@@ -1,264 +0,0 @@
-SHA_BLOCKSIZE = 64
-SHA_DIGESTSIZE = 32
-
-
-def new_shaobject():
-    return {
-        'digest': [0]*8,
-        'count_lo': 0,
-        'count_hi': 0,
-        'data': [0]* SHA_BLOCKSIZE,
-        'local': 0,
-        'digestsize': 0
-    }
-
-ROR = lambda x, y: (((x & 0xffffffff) >> (y & 31)) | (x << (32 - (y & 31)))) & 0xffffffff
-Ch = lambda x, y, z: (z ^ (x & (y ^ z)))
-Maj = lambda x, y, z: (((x | y) & z) | (x & y))
-S = lambda x, n: ROR(x, n)
-R = lambda x, n: (x & 0xffffffff) >> n
-Sigma0 = lambda x: (S(x, 2) ^ S(x, 13) ^ S(x, 22))
-Sigma1 = lambda x: (S(x, 6) ^ S(x, 11) ^ S(x, 25))
-Gamma0 = lambda x: (S(x, 7) ^ S(x, 18) ^ R(x, 3))
-Gamma1 = lambda x: (S(x, 17) ^ S(x, 19) ^ R(x, 10))
-
-def sha_transform(sha_info):
-    W = []
-    
-    d = sha_info['data']
-    for i in range(0,16):
-        W.append( (d[4*i]<<24) + (d[4*i+1]<<16) + (d[4*i+2]<<8) + d[4*i+3])
-    
-    for i in range(16,64):
-        W.append( (Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]) & 0xffffffff )
-    
-    ss = sha_info['digest'][:]
-    
-    def RND(a,b,c,d,e,f,g,h,i,ki):
-        t0 = h + Sigma1(e) + Ch(e, f, g) + ki + W[i];
-        t1 = Sigma0(a) + Maj(a, b, c);
-        d += t0;
-        h  = t0 + t1;
-        return d & 0xffffffff, h & 0xffffffff
-    
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],0,0x428a2f98);
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],1,0x71374491);
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],2,0xb5c0fbcf);
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],3,0xe9b5dba5);
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],4,0x3956c25b);
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],5,0x59f111f1);
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],6,0x923f82a4);
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],7,0xab1c5ed5);
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],8,0xd807aa98);
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],9,0x12835b01);
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],10,0x243185be);
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],11,0x550c7dc3);
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],12,0x72be5d74);
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],13,0x80deb1fe);
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],14,0x9bdc06a7);
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],15,0xc19bf174);
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],16,0xe49b69c1);
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],17,0xefbe4786);
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],18,0x0fc19dc6);
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],19,0x240ca1cc);
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],20,0x2de92c6f);
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],21,0x4a7484aa);
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],22,0x5cb0a9dc);
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],23,0x76f988da);
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],24,0x983e5152);
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],25,0xa831c66d);
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],26,0xb00327c8);
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],27,0xbf597fc7);
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],28,0xc6e00bf3);
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],29,0xd5a79147);
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],30,0x06ca6351);
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],31,0x14292967);
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],32,0x27b70a85);
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],33,0x2e1b2138);
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],34,0x4d2c6dfc);
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],35,0x53380d13);
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],36,0x650a7354);
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],37,0x766a0abb);
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],38,0x81c2c92e);
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],39,0x92722c85);
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],40,0xa2bfe8a1);
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],41,0xa81a664b);
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],42,0xc24b8b70);
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],43,0xc76c51a3);
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],44,0xd192e819);
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],45,0xd6990624);
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],46,0xf40e3585);
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],47,0x106aa070);
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],48,0x19a4c116);
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],49,0x1e376c08);
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],50,0x2748774c);
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],51,0x34b0bcb5);
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],52,0x391c0cb3);
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],53,0x4ed8aa4a);
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],54,0x5b9cca4f);
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],55,0x682e6ff3);
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],56,0x748f82ee);
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],57,0x78a5636f);
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],58,0x84c87814);
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],59,0x8cc70208);
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],60,0x90befffa);
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],61,0xa4506ceb);
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],62,0xbef9a3f7);
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],63,0xc67178f2);
-    
-    dig = []
-    for i, x in enumerate(sha_info['digest']):
-        dig.append( (x + ss[i]) & 0xffffffff )
-    sha_info['digest'] = dig
-
-def sha_init():
-    sha_info = new_shaobject()
-    sha_info['digest'] = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]
-    sha_info['count_lo'] = 0
-    sha_info['count_hi'] = 0
-    sha_info['local'] = 0
-    sha_info['digestsize'] = 32
-    return sha_info
-
-def sha224_init():
-    sha_info = new_shaobject()
-    sha_info['digest'] = [0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4]
-    sha_info['count_lo'] = 0
-    sha_info['count_hi'] = 0
-    sha_info['local'] = 0
-    sha_info['digestsize'] = 28
-    return sha_info
-
-def getbuf(s):
-    if isinstance(s, str):
-        return s.encode('ascii')
-    else:
-        return bytes(s)
-
-def sha_update(sha_info, buffer):
-    if isinstance(buffer, str):
-        raise TypeError("Unicode strings must be encoded before hashing")
-    count = len(buffer)
-    buffer_idx = 0
-    clo = (sha_info['count_lo'] + (count << 3)) & 0xffffffff
-    if clo < sha_info['count_lo']:
-        sha_info['count_hi'] += 1
-    sha_info['count_lo'] = clo
-    
-    sha_info['count_hi'] += (count >> 29)
-    
-    if sha_info['local']:
-        i = SHA_BLOCKSIZE - sha_info['local']
-        if i > count:
-            i = count
-        
-        # copy buffer
-        for x in enumerate(buffer[buffer_idx:buffer_idx+i]):
-            sha_info['data'][sha_info['local']+x[0]] = x[1]
-        
-        count -= i
-        buffer_idx += i
-        
-        sha_info['local'] += i
-        if sha_info['local'] == SHA_BLOCKSIZE:
-            sha_transform(sha_info)
-            sha_info['local'] = 0
-        else:
-            return
-    
-    while count >= SHA_BLOCKSIZE:
-        # copy buffer
-        sha_info['data'] = list(buffer[buffer_idx:buffer_idx + SHA_BLOCKSIZE])
-        count -= SHA_BLOCKSIZE
-        buffer_idx += SHA_BLOCKSIZE
-        sha_transform(sha_info)
-        
-    
-    # copy buffer
-    pos = sha_info['local']
-    sha_info['data'][pos:pos+count] = list(buffer[buffer_idx:buffer_idx + count])
-    sha_info['local'] = count
-
-def sha_final(sha_info):
-    lo_bit_count = sha_info['count_lo']
-    hi_bit_count = sha_info['count_hi']
-    count = (lo_bit_count >> 3) & 0x3f
-    sha_info['data'][count] = 0x80;
-    count += 1
-    if count > SHA_BLOCKSIZE - 8:
-        # zero the bytes in data after the count
-        sha_info['data'] = sha_info['data'][:count] + ([0] * (SHA_BLOCKSIZE - count))
-        sha_transform(sha_info)
-        # zero bytes in data
-        sha_info['data'] = [0] * SHA_BLOCKSIZE
-    else:
-        sha_info['data'] = sha_info['data'][:count] + ([0] * (SHA_BLOCKSIZE - count))
-    
-    sha_info['data'][56] = (hi_bit_count >> 24) & 0xff
-    sha_info['data'][57] = (hi_bit_count >> 16) & 0xff
-    sha_info['data'][58] = (hi_bit_count >>  8) & 0xff
-    sha_info['data'][59] = (hi_bit_count >>  0) & 0xff
-    sha_info['data'][60] = (lo_bit_count >> 24) & 0xff
-    sha_info['data'][61] = (lo_bit_count >> 16) & 0xff
-    sha_info['data'][62] = (lo_bit_count >>  8) & 0xff
-    sha_info['data'][63] = (lo_bit_count >>  0) & 0xff
-    
-    sha_transform(sha_info)
-    
-    dig = []
-    for i in sha_info['digest']:
-        dig.extend([ ((i>>24) & 0xff), ((i>>16) & 0xff), ((i>>8) & 0xff), (i & 0xff) ])
-    return bytes(dig)
-
-class sha256(object):
-    digest_size = digestsize = SHA_DIGESTSIZE
-    block_size = SHA_BLOCKSIZE
-
-    def __init__(self, s=None):
-        self._sha = sha_init()
-        if s:
-            sha_update(self._sha, getbuf(s))
-    
-    def update(self, s):
-        sha_update(self._sha, getbuf(s))
-    
-    def digest(self):
-        return sha_final(self._sha.copy())[:self._sha['digestsize']]
-    
-    def hexdigest(self):
-        return ''.join(['%.2x' % i for i in self.digest()])
-
-    def copy(self):
-        new = sha256()
-        new._sha = self._sha.copy()
-        return new
-
-class sha224(sha256):
-    digest_size = digestsize = 28
-
-    def __init__(self, s=None):
-        self._sha = sha224_init()
-        if s:
-            sha_update(self._sha, getbuf(s))
-
-    def copy(self):
-        new = sha224()
-        new._sha = self._sha.copy()
-        return new
-
-def test():
-    a_str = "just a test string"
-    
-    assert b"\xe3\xb0\xc4B\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99o\xb9$'\xaeA\xe4d\x9b\x93L\xa4\x95\x99\x1bxR\xb8U" == sha256().digest()
-    assert 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' == sha256().hexdigest()
-    assert 'd7b553c6f09ac85d142415f857c5310f3bbbe7cdd787cce4b985acedd585266f' == sha256(a_str).hexdigest()
-    assert '8113ebf33c97daa9998762aacafe750c7cefc2b2f173c90c59663a57fe626f21' == sha256(a_str*7).hexdigest()
-    
-    s = sha256(a_str)
-    s.update(a_str)
-    assert '03d9963e05a094593190b6fc794cb1a3e1ac7d7883f0b5855268afeccc70d461' == s.hexdigest()
-
-if __name__ == "__main__":
-    test()
-
-
diff --git a/hashlib/hashlib/sha384.py b/hashlib/hashlib/sha384.py
deleted file mode 100644
index 5a9fd1dfd..000000000
--- a/hashlib/hashlib/sha384.py
+++ /dev/null
@@ -1 +0,0 @@
-from .sha512 import sha384
diff --git a/hashlib/hashlib/sha512.py b/hashlib/hashlib/sha512.py
deleted file mode 100644
index 8875db2e5..000000000
--- a/hashlib/hashlib/sha512.py
+++ /dev/null
@@ -1,290 +0,0 @@
-"""
-This code was Ported from CPython's sha512module.c
-"""
-
-SHA_BLOCKSIZE = 128
-SHA_DIGESTSIZE = 64
-
-
-def new_shaobject():
-    return {
-        'digest': [0]*8,
-        'count_lo': 0,
-        'count_hi': 0,
-        'data': [0]* SHA_BLOCKSIZE,
-        'local': 0,
-        'digestsize': 0
-    }
-
-ROR64 = lambda x, y: (((x & 0xffffffffffffffff) >> (y & 63)) | (x << (64 - (y & 63)))) & 0xffffffffffffffff
-Ch = lambda x, y, z: (z ^ (x & (y ^ z)))
-Maj = lambda x, y, z: (((x | y) & z) | (x & y))
-S = lambda x, n: ROR64(x, n)
-R = lambda x, n: (x & 0xffffffffffffffff) >> n
-Sigma0 = lambda x: (S(x, 28) ^ S(x, 34) ^ S(x, 39))
-Sigma1 = lambda x: (S(x, 14) ^ S(x, 18) ^ S(x, 41))
-Gamma0 = lambda x: (S(x, 1) ^ S(x, 8) ^ R(x, 7))
-Gamma1 = lambda x: (S(x, 19) ^ S(x, 61) ^ R(x, 6))
-
-def sha_transform(sha_info):
-    W = []
-
-    d = sha_info['data']
-    for i in range(0,16):
-        W.append( (d[8*i]<<56) + (d[8*i+1]<<48) + (d[8*i+2]<<40) + (d[8*i+3]<<32) + (d[8*i+4]<<24) + (d[8*i+5]<<16) + (d[8*i+6]<<8) + d[8*i+7])
-
-    for i in range(16,80):
-        W.append( (Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]) & 0xffffffffffffffff )
-
-    ss = sha_info['digest'][:]
-
-    def RND(a,b,c,d,e,f,g,h,i,ki):
-        t0 = (h + Sigma1(e) + Ch(e, f, g) + ki + W[i]) & 0xffffffffffffffff
-        t1 = (Sigma0(a) + Maj(a, b, c)) & 0xffffffffffffffff
-        d = (d + t0) & 0xffffffffffffffff
-        h = (t0 + t1) & 0xffffffffffffffff
-        return d & 0xffffffffffffffff, h & 0xffffffffffffffff
-
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],0,0x428a2f98d728ae22)
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],1,0x7137449123ef65cd)
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],2,0xb5c0fbcfec4d3b2f)
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],3,0xe9b5dba58189dbbc)
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],4,0x3956c25bf348b538)
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],5,0x59f111f1b605d019)
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],6,0x923f82a4af194f9b)
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],7,0xab1c5ed5da6d8118)
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],8,0xd807aa98a3030242)
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],9,0x12835b0145706fbe)
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],10,0x243185be4ee4b28c)
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],11,0x550c7dc3d5ffb4e2)
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],12,0x72be5d74f27b896f)
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],13,0x80deb1fe3b1696b1)
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],14,0x9bdc06a725c71235)
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],15,0xc19bf174cf692694)
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],16,0xe49b69c19ef14ad2)
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],17,0xefbe4786384f25e3)
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],18,0x0fc19dc68b8cd5b5)
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],19,0x240ca1cc77ac9c65)
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],20,0x2de92c6f592b0275)
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],21,0x4a7484aa6ea6e483)
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],22,0x5cb0a9dcbd41fbd4)
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],23,0x76f988da831153b5)
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],24,0x983e5152ee66dfab)
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],25,0xa831c66d2db43210)
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],26,0xb00327c898fb213f)
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],27,0xbf597fc7beef0ee4)
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],28,0xc6e00bf33da88fc2)
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],29,0xd5a79147930aa725)
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],30,0x06ca6351e003826f)
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],31,0x142929670a0e6e70)
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],32,0x27b70a8546d22ffc)
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],33,0x2e1b21385c26c926)
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],34,0x4d2c6dfc5ac42aed)
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],35,0x53380d139d95b3df)
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],36,0x650a73548baf63de)
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],37,0x766a0abb3c77b2a8)
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],38,0x81c2c92e47edaee6)
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],39,0x92722c851482353b)
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],40,0xa2bfe8a14cf10364)
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],41,0xa81a664bbc423001)
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],42,0xc24b8b70d0f89791)
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],43,0xc76c51a30654be30)
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],44,0xd192e819d6ef5218)
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],45,0xd69906245565a910)
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],46,0xf40e35855771202a)
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],47,0x106aa07032bbd1b8)
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],48,0x19a4c116b8d2d0c8)
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],49,0x1e376c085141ab53)
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],50,0x2748774cdf8eeb99)
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],51,0x34b0bcb5e19b48a8)
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],52,0x391c0cb3c5c95a63)
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],53,0x4ed8aa4ae3418acb)
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],54,0x5b9cca4f7763e373)
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],55,0x682e6ff3d6b2b8a3)
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],56,0x748f82ee5defb2fc)
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],57,0x78a5636f43172f60)
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],58,0x84c87814a1f0ab72)
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],59,0x8cc702081a6439ec)
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],60,0x90befffa23631e28)
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],61,0xa4506cebde82bde9)
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],62,0xbef9a3f7b2c67915)
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],63,0xc67178f2e372532b)
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],64,0xca273eceea26619c)
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],65,0xd186b8c721c0c207)
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],66,0xeada7dd6cde0eb1e)
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],67,0xf57d4f7fee6ed178)
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],68,0x06f067aa72176fba)
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],69,0x0a637dc5a2c898a6)
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],70,0x113f9804bef90dae)
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],71,0x1b710b35131c471b)
-    ss[3], ss[7] = RND(ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],72,0x28db77f523047d84)
-    ss[2], ss[6] = RND(ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],73,0x32caab7b40c72493)
-    ss[1], ss[5] = RND(ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],ss[5],74,0x3c9ebe0a15c9bebc)
-    ss[0], ss[4] = RND(ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],ss[4],75,0x431d67c49c100d4c)
-    ss[7], ss[3] = RND(ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],ss[3],76,0x4cc5d4becb3e42b6)
-    ss[6], ss[2] = RND(ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],ss[2],77,0x597f299cfc657e2a)
-    ss[5], ss[1] = RND(ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],ss[1],78,0x5fcb6fab3ad6faec)
-    ss[4], ss[0] = RND(ss[1],ss[2],ss[3],ss[4],ss[5],ss[6],ss[7],ss[0],79,0x6c44198c4a475817)
-
-    dig = []
-    for i, x in enumerate(sha_info['digest']):
-        dig.append( (x + ss[i]) & 0xffffffffffffffff )
-    sha_info['digest'] = dig
-
-def sha_init():
-    sha_info = new_shaobject()
-    sha_info['digest'] = [ 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179]
-    sha_info['count_lo'] = 0
-    sha_info['count_hi'] = 0
-    sha_info['local'] = 0
-    sha_info['digestsize'] = 64
-    return sha_info
-
-def sha384_init():
-    sha_info = new_shaobject()
-    sha_info['digest'] = [ 0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4]
-    sha_info['count_lo'] = 0
-    sha_info['count_hi'] = 0
-    sha_info['local'] = 0
-    sha_info['digestsize'] = 48
-    return sha_info
-
-def getbuf(s):
-    if isinstance(s, str):
-        return s.encode('ascii')
-    else:
-        return bytes(s)
-
-def sha_update(sha_info, buffer):
-    if isinstance(buffer, str):
-        raise TypeError("Unicode strings must be encoded before hashing")
-    count = len(buffer)
-    buffer_idx = 0
-    clo = (sha_info['count_lo'] + (count << 3)) & 0xffffffff
-    if clo < sha_info['count_lo']:
-        sha_info['count_hi'] += 1
-    sha_info['count_lo'] = clo
-
-    sha_info['count_hi'] += (count >> 29)
-
-    if sha_info['local']:
-        i = SHA_BLOCKSIZE - sha_info['local']
-        if i > count:
-            i = count
-
-        # copy buffer
-        for x in enumerate(buffer[buffer_idx:buffer_idx+i]):
-            sha_info['data'][sha_info['local']+x[0]] = x[1]
-
-        count -= i
-        buffer_idx += i
-
-        sha_info['local'] += i
-        if sha_info['local'] == SHA_BLOCKSIZE:
-            sha_transform(sha_info)
-            sha_info['local'] = 0
-        else:
-            return
-
-    while count >= SHA_BLOCKSIZE:
-        # copy buffer
-        sha_info['data'] = list(buffer[buffer_idx:buffer_idx + SHA_BLOCKSIZE])
-        count -= SHA_BLOCKSIZE
-        buffer_idx += SHA_BLOCKSIZE
-        sha_transform(sha_info)
-
-    # copy buffer
-    pos = sha_info['local']
-    sha_info['data'][pos:pos+count] = list(buffer[buffer_idx:buffer_idx + count])
-    sha_info['local'] = count
-
-def sha_final(sha_info):
-    lo_bit_count = sha_info['count_lo']
-    hi_bit_count = sha_info['count_hi']
-    count = (lo_bit_count >> 3) & 0x7f
-    sha_info['data'][count] = 0x80;
-    count += 1
-    if count > SHA_BLOCKSIZE - 16:
-        # zero the bytes in data after the count
-        sha_info['data'] = sha_info['data'][:count] + ([0] * (SHA_BLOCKSIZE - count))
-        sha_transform(sha_info)
-        # zero bytes in data
-        sha_info['data'] = [0] * SHA_BLOCKSIZE
-    else:
-        sha_info['data'] = sha_info['data'][:count] + ([0] * (SHA_BLOCKSIZE - count))
-
-    sha_info['data'][112] = 0;
-    sha_info['data'][113] = 0;
-    sha_info['data'][114] = 0;
-    sha_info['data'][115] = 0;
-    sha_info['data'][116] = 0;
-    sha_info['data'][117] = 0;
-    sha_info['data'][118] = 0;
-    sha_info['data'][119] = 0;
-
-    sha_info['data'][120] = (hi_bit_count >> 24) & 0xff
-    sha_info['data'][121] = (hi_bit_count >> 16) & 0xff
-    sha_info['data'][122] = (hi_bit_count >>  8) & 0xff
-    sha_info['data'][123] = (hi_bit_count >>  0) & 0xff
-    sha_info['data'][124] = (lo_bit_count >> 24) & 0xff
-    sha_info['data'][125] = (lo_bit_count >> 16) & 0xff
-    sha_info['data'][126] = (lo_bit_count >>  8) & 0xff
-    sha_info['data'][127] = (lo_bit_count >>  0) & 0xff
-
-    sha_transform(sha_info)
-
-    dig = []
-    for i in sha_info['digest']:
-        dig.extend([ ((i>>56) & 0xff), ((i>>48) & 0xff), ((i>>40) & 0xff), ((i>>32) & 0xff), ((i>>24) & 0xff), ((i>>16) & 0xff), ((i>>8) & 0xff), (i & 0xff) ])
-    return bytes(dig)
-
-class sha512(object):
-    digest_size = digestsize = SHA_DIGESTSIZE
-    block_size = SHA_BLOCKSIZE
-
-    def __init__(self, s=None):
-        self._sha = sha_init()
-        if s:
-            sha_update(self._sha, getbuf(s))
-
-    def update(self, s):
-        sha_update(self._sha, getbuf(s))
-
-    def digest(self):
-        return sha_final(self._sha.copy())[:self._sha['digestsize']]
-
-    def hexdigest(self):
-        return ''.join(['%.2x' % i for i in self.digest()])
-
-    def copy(self):
-        new = sha512()
-        new._sha = self._sha.copy()
-        return new
-
-class sha384(sha512):
-    digest_size = digestsize = 48
-
-    def __init__(self, s=None):
-        self._sha = sha384_init()
-        if s:
-            sha_update(self._sha, getbuf(s))
-
-    def copy(self):
-        new = sha384()
-        new._sha = self._sha.copy()
-        return new
-
-def test():
-    a_str = "just a test string"
-
-    assert sha512().digest() == b"\xcf\x83\xe15~\xef\xb8\xbd\xf1T(P\xd6m\x80\x07\xd6 \xe4\x05\x0bW\x15\xdc\x83\xf4\xa9!\xd3l\xe9\xceG\xd0\xd1<]\x85\xf2\xb0\xff\x83\x18\xd2\x87~\xec/c\xb91\xbdGAz\x81\xa582z\xf9'\xda>"
-    assert sha512().hexdigest() == 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e'
-    assert sha512(a_str).hexdigest() == '68be4c6664af867dd1d01c8d77e963d87d77b702400c8fabae355a41b8927a5a5533a7f1c28509bbd65c5f3ac716f33be271fbda0ca018b71a84708c9fae8a53'
-    assert sha512(a_str*7).hexdigest() == '3233acdbfcfff9bff9fc72401d31dbffa62bd24e9ec846f0578d647da73258d9f0879f7fde01fe2cc6516af3f343807fdef79e23d696c923d79931db46bf1819'
-
-    s = sha512(a_str)
-    s.update(a_str)
-    assert s.hexdigest() == '341aeb668730bbb48127d5531115f3c39d12cb9586a6ca770898398aff2411087cfe0b570689adf328cddeb1f00803acce6737a19f310b53bbdb0320828f75bb'
-
-if __name__ == "__main__":
-    test()
diff --git a/hashlib/metadata.txt b/hashlib/metadata.txt
deleted file mode 100644
index 63be0a56a..000000000
--- a/hashlib/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=pypy
-type=package
-version=2.4.0-2
diff --git a/hashlib/setup.py b/hashlib/setup.py
deleted file mode 100644
index ac207a4f5..000000000
--- a/hashlib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-hashlib',
-      version='2.4.0-2',
-      description='PyPy hashlib module ported to MicroPython',
-      long_description='This is a module ported from PyPy standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='PyPy Developers',
-      author_email='pypy-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['hashlib'])
diff --git a/hashlib/test_hashlib.py b/hashlib/test_hashlib.py
deleted file mode 100644
index 9f8dec58a..000000000
--- a/hashlib/test_hashlib.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from hashlib.sha256 import test as sha256_test
-from hashlib.sha512 import test as sha512_test
-
-
-sha256_test()
-sha512_test()
-print("OK")
diff --git a/heapq/metadata.txt b/heapq/metadata.txt
deleted file mode 100644
index 568746523..000000000
--- a/heapq/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = cpython
-type = module
-version = 0.9.2
-# Module uses in *some* functions, but we don't want to depend on it
-#depends = itertools
diff --git a/heapq/setup.py b/heapq/setup.py
deleted file mode 100644
index 446c1610e..000000000
--- a/heapq/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-heapq',
-      version='0.9.2',
-      description='CPython heapq module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['heapq'])
diff --git a/hmac/hmac.py b/hmac/hmac.py
deleted file mode 100644
index c2ce23b11..000000000
--- a/hmac/hmac.py
+++ /dev/null
@@ -1,149 +0,0 @@
-"""HMAC (Keyed-Hashing for Message Authentication) Python module.
-
-Implements the HMAC algorithm as described by RFC 2104.
-"""
-
-import warnings as _warnings
-#from _operator import _compare_digest as compare_digest
-import hashlib as _hashlib
-PendingDeprecationWarning = None
-RuntimeWarning = None
-
-trans_5C = bytes((x ^ 0x5C) for x in range(256))
-trans_36 = bytes((x ^ 0x36) for x in range(256))
-
-def translate(d, t):
-    return b''.join([ chr(t[x]).encode('ascii') for x in d ])
-
-# The size of the digests returned by HMAC depends on the underlying
-# hashing module used.  Use digest_size from the instance of HMAC instead.
-digest_size = None
-
-
-
-class HMAC:
-    """RFC 2104 HMAC class.  Also complies with RFC 4231.
-
-    This supports the API for Cryptographic Hash Functions (PEP 247).
-    """
-    blocksize = 64  # 512-bit HMAC; can be changed in subclasses.
-
-    def __init__(self, key, msg = None, digestmod = None):
-        """Create a new HMAC object.
-
-        key:       key for the keyed hash object.
-        msg:       Initial input for the hash, if provided.
-        digestmod: A module supporting PEP 247.  *OR*
-                   A hashlib constructor returning a new hash object. *OR*
-                   A hash name suitable for hashlib.new().
-                   Defaults to hashlib.md5.
-                   Implicit default to hashlib.md5 is deprecated and will be
-                   removed in Python 3.6.
-
-        Note: key and msg must be a bytes or bytearray objects.
-        """
-
-        if not isinstance(key, (bytes, bytearray)):
-            raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__)
-
-        if digestmod is None:
-            _warnings.warn("HMAC() without an explicit digestmod argument "
-                           "is deprecated.", PendingDeprecationWarning, 2)
-            digestmod = _hashlib.md5
-
-        if callable(digestmod):
-            self.digest_cons = digestmod
-        elif isinstance(digestmod, str):
-            self.digest_cons = lambda d=b'': _hashlib.new(digestmod, d)
-        else:
-            self.digest_cons = lambda d=b'': digestmod.new(d)
-
-        self.outer = self.digest_cons()
-        self.inner = self.digest_cons()
-        self.digest_size = self.inner.digest_size
-
-        if hasattr(self.inner, 'block_size'):
-            blocksize = self.inner.block_size
-            if blocksize < 16:
-                _warnings.warn('block_size of %d seems too small; using our '
-                               'default of %d.' % (blocksize, self.blocksize),
-                               RuntimeWarning, 2)
-                blocksize = self.blocksize
-        else:
-            _warnings.warn('No block_size attribute on given digest object; '
-                           'Assuming %d.' % (self.blocksize),
-                           RuntimeWarning, 2)
-            blocksize = self.blocksize
-
-        # self.blocksize is the default blocksize. self.block_size is
-        # effective block size as well as the public API attribute.
-        self.block_size = blocksize
-
-        if len(key) > blocksize:
-            key = self.digest_cons(key).digest()
-
-        key = key + bytes(blocksize - len(key))
-        self.outer.update(translate(key, trans_5C))
-        self.inner.update(translate(key, trans_36))
-        if msg is not None:
-            self.update(msg)
-
-    @property
-    def name(self):
-        return "hmac-" + self.inner.name
-
-    def update(self, msg):
-        """Update this hashing object with the string msg.
-        """
-        self.inner.update(msg)
-
-    def copy(self):
-        """Return a separate copy of this hashing object.
-
-        An update to this copy won't affect the original object.
-        """
-        # Call __new__ directly to avoid the expensive __init__.
-        other = self.__class__.__new__(self.__class__)
-        other.digest_cons = self.digest_cons
-        other.digest_size = self.digest_size
-        other.inner = self.inner.copy()
-        other.outer = self.outer.copy()
-        return other
-
-    def _current(self):
-        """Return a hash object for the current state.
-
-        To be used only internally with digest() and hexdigest().
-        """
-        h = self.outer.copy()
-        h.update(self.inner.digest())
-        return h
-
-    def digest(self):
-        """Return the hash value of this hashing object.
-
-        This returns a string containing 8-bit data.  The object is
-        not altered in any way by this function; you can continue
-        updating the object after calling this function.
-        """
-        h = self._current()
-        return h.digest()
-
-    def hexdigest(self):
-        """Like digest(), but returns a string of hexadecimal digits instead.
-        """
-        h = self._current()
-        return h.hexdigest()
-
-def new(key, msg = None, digestmod = None):
-    """Create a new hashing object and return it.
-
-    key: The starting key for the hash.
-    msg: if available, will immediately be hashed into the object's starting
-    state.
-
-    You can now feed arbitrary strings into the object using its update()
-    method, and can ask for the hash value at any time by calling its digest()
-    method.
-    """
-    return HMAC(key, msg, digestmod)
diff --git a/hmac/metadata.txt b/hmac/metadata.txt
deleted file mode 100644
index a311314c2..000000000
--- a/hmac/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = module
-version = 3.4.2-1
-depends = warnings, hashlib
diff --git a/hmac/setup.py b/hmac/setup.py
deleted file mode 100644
index 5db37cbcf..000000000
--- a/hmac/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-hmac',
-      version='3.4.2-1',
-      description='CPython hmac module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['hmac'],
-      install_requires=['micropython-warnings', 'micropython-hashlib'])
diff --git a/hmac/test_hmac.py b/hmac/test_hmac.py
deleted file mode 100644
index a2c1349c0..000000000
--- a/hmac/test_hmac.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import hmac
-from hashlib.sha256 import sha256
-from hashlib.sha512 import sha512
-
-msg = b'zlutoucky kun upel dabelske ody'
-
-dig = hmac.new(b'1234567890', msg=msg, digestmod=sha256).hexdigest()
-
-print('c735e751e36b08fb01e25794bdb15e7289b82aecdb652c8f4f72f307b39dad39')
-print(dig)
-
-if dig != 'c735e751e36b08fb01e25794bdb15e7289b82aecdb652c8f4f72f307b39dad39':
-    raise Exception("Error")
-
-dig = hmac.new(b'1234567890', msg=msg, digestmod=sha512).hexdigest()
-
-print('59942f31b6f5473fb4eb630fabf5358a49bc11d24ebc83b114b4af30d6ef47ea14b673f478586f520a0b9c53b27c8f8dd618c165ef586195bd4e98293d34df1a')
-print(dig)
-
-if dig != '59942f31b6f5473fb4eb630fabf5358a49bc11d24ebc83b114b4af30d6ef47ea14b673f478586f520a0b9c53b27c8f8dd618c165ef586195bd4e98293d34df1a':
-    raise Exception("Error")
-
diff --git a/html.entities/html/entities.py b/html.entities/html/entities.py
deleted file mode 100644
index e891ad659..000000000
--- a/html.entities/html/entities.py
+++ /dev/null
@@ -1,2506 +0,0 @@
-"""HTML character entity references."""
-
-# maps the HTML entity name to the Unicode codepoint
-name2codepoint = {
-    'AElig':    0x00c6, # latin capital letter AE = latin capital ligature AE, U+00C6 ISOlat1
-    'Aacute':   0x00c1, # latin capital letter A with acute, U+00C1 ISOlat1
-    'Acirc':    0x00c2, # latin capital letter A with circumflex, U+00C2 ISOlat1
-    'Agrave':   0x00c0, # latin capital letter A with grave = latin capital letter A grave, U+00C0 ISOlat1
-    'Alpha':    0x0391, # greek capital letter alpha, U+0391
-    'Aring':    0x00c5, # latin capital letter A with ring above = latin capital letter A ring, U+00C5 ISOlat1
-    'Atilde':   0x00c3, # latin capital letter A with tilde, U+00C3 ISOlat1
-    'Auml':     0x00c4, # latin capital letter A with diaeresis, U+00C4 ISOlat1
-    'Beta':     0x0392, # greek capital letter beta, U+0392
-    'Ccedil':   0x00c7, # latin capital letter C with cedilla, U+00C7 ISOlat1
-    'Chi':      0x03a7, # greek capital letter chi, U+03A7
-    'Dagger':   0x2021, # double dagger, U+2021 ISOpub
-    'Delta':    0x0394, # greek capital letter delta, U+0394 ISOgrk3
-    'ETH':      0x00d0, # latin capital letter ETH, U+00D0 ISOlat1
-    'Eacute':   0x00c9, # latin capital letter E with acute, U+00C9 ISOlat1
-    'Ecirc':    0x00ca, # latin capital letter E with circumflex, U+00CA ISOlat1
-    'Egrave':   0x00c8, # latin capital letter E with grave, U+00C8 ISOlat1
-    'Epsilon':  0x0395, # greek capital letter epsilon, U+0395
-    'Eta':      0x0397, # greek capital letter eta, U+0397
-    'Euml':     0x00cb, # latin capital letter E with diaeresis, U+00CB ISOlat1
-    'Gamma':    0x0393, # greek capital letter gamma, U+0393 ISOgrk3
-    'Iacute':   0x00cd, # latin capital letter I with acute, U+00CD ISOlat1
-    'Icirc':    0x00ce, # latin capital letter I with circumflex, U+00CE ISOlat1
-    'Igrave':   0x00cc, # latin capital letter I with grave, U+00CC ISOlat1
-    'Iota':     0x0399, # greek capital letter iota, U+0399
-    'Iuml':     0x00cf, # latin capital letter I with diaeresis, U+00CF ISOlat1
-    'Kappa':    0x039a, # greek capital letter kappa, U+039A
-    'Lambda':   0x039b, # greek capital letter lambda, U+039B ISOgrk3
-    'Mu':       0x039c, # greek capital letter mu, U+039C
-    'Ntilde':   0x00d1, # latin capital letter N with tilde, U+00D1 ISOlat1
-    'Nu':       0x039d, # greek capital letter nu, U+039D
-    'OElig':    0x0152, # latin capital ligature OE, U+0152 ISOlat2
-    'Oacute':   0x00d3, # latin capital letter O with acute, U+00D3 ISOlat1
-    'Ocirc':    0x00d4, # latin capital letter O with circumflex, U+00D4 ISOlat1
-    'Ograve':   0x00d2, # latin capital letter O with grave, U+00D2 ISOlat1
-    'Omega':    0x03a9, # greek capital letter omega, U+03A9 ISOgrk3
-    'Omicron':  0x039f, # greek capital letter omicron, U+039F
-    'Oslash':   0x00d8, # latin capital letter O with stroke = latin capital letter O slash, U+00D8 ISOlat1
-    'Otilde':   0x00d5, # latin capital letter O with tilde, U+00D5 ISOlat1
-    'Ouml':     0x00d6, # latin capital letter O with diaeresis, U+00D6 ISOlat1
-    'Phi':      0x03a6, # greek capital letter phi, U+03A6 ISOgrk3
-    'Pi':       0x03a0, # greek capital letter pi, U+03A0 ISOgrk3
-    'Prime':    0x2033, # double prime = seconds = inches, U+2033 ISOtech
-    'Psi':      0x03a8, # greek capital letter psi, U+03A8 ISOgrk3
-    'Rho':      0x03a1, # greek capital letter rho, U+03A1
-    'Scaron':   0x0160, # latin capital letter S with caron, U+0160 ISOlat2
-    'Sigma':    0x03a3, # greek capital letter sigma, U+03A3 ISOgrk3
-    'THORN':    0x00de, # latin capital letter THORN, U+00DE ISOlat1
-    'Tau':      0x03a4, # greek capital letter tau, U+03A4
-    'Theta':    0x0398, # greek capital letter theta, U+0398 ISOgrk3
-    'Uacute':   0x00da, # latin capital letter U with acute, U+00DA ISOlat1
-    'Ucirc':    0x00db, # latin capital letter U with circumflex, U+00DB ISOlat1
-    'Ugrave':   0x00d9, # latin capital letter U with grave, U+00D9 ISOlat1
-    'Upsilon':  0x03a5, # greek capital letter upsilon, U+03A5 ISOgrk3
-    'Uuml':     0x00dc, # latin capital letter U with diaeresis, U+00DC ISOlat1
-    'Xi':       0x039e, # greek capital letter xi, U+039E ISOgrk3
-    'Yacute':   0x00dd, # latin capital letter Y with acute, U+00DD ISOlat1
-    'Yuml':     0x0178, # latin capital letter Y with diaeresis, U+0178 ISOlat2
-    'Zeta':     0x0396, # greek capital letter zeta, U+0396
-    'aacute':   0x00e1, # latin small letter a with acute, U+00E1 ISOlat1
-    'acirc':    0x00e2, # latin small letter a with circumflex, U+00E2 ISOlat1
-    'acute':    0x00b4, # acute accent = spacing acute, U+00B4 ISOdia
-    'aelig':    0x00e6, # latin small letter ae = latin small ligature ae, U+00E6 ISOlat1
-    'agrave':   0x00e0, # latin small letter a with grave = latin small letter a grave, U+00E0 ISOlat1
-    'alefsym':  0x2135, # alef symbol = first transfinite cardinal, U+2135 NEW
-    'alpha':    0x03b1, # greek small letter alpha, U+03B1 ISOgrk3
-    'amp':      0x0026, # ampersand, U+0026 ISOnum
-    'and':      0x2227, # logical and = wedge, U+2227 ISOtech
-    'ang':      0x2220, # angle, U+2220 ISOamso
-    'aring':    0x00e5, # latin small letter a with ring above = latin small letter a ring, U+00E5 ISOlat1
-    'asymp':    0x2248, # almost equal to = asymptotic to, U+2248 ISOamsr
-    'atilde':   0x00e3, # latin small letter a with tilde, U+00E3 ISOlat1
-    'auml':     0x00e4, # latin small letter a with diaeresis, U+00E4 ISOlat1
-    'bdquo':    0x201e, # double low-9 quotation mark, U+201E NEW
-    'beta':     0x03b2, # greek small letter beta, U+03B2 ISOgrk3
-    'brvbar':   0x00a6, # broken bar = broken vertical bar, U+00A6 ISOnum
-    'bull':     0x2022, # bullet = black small circle, U+2022 ISOpub
-    'cap':      0x2229, # intersection = cap, U+2229 ISOtech
-    'ccedil':   0x00e7, # latin small letter c with cedilla, U+00E7 ISOlat1
-    'cedil':    0x00b8, # cedilla = spacing cedilla, U+00B8 ISOdia
-    'cent':     0x00a2, # cent sign, U+00A2 ISOnum
-    'chi':      0x03c7, # greek small letter chi, U+03C7 ISOgrk3
-    'circ':     0x02c6, # modifier letter circumflex accent, U+02C6 ISOpub
-    'clubs':    0x2663, # black club suit = shamrock, U+2663 ISOpub
-    'cong':     0x2245, # approximately equal to, U+2245 ISOtech
-    'copy':     0x00a9, # copyright sign, U+00A9 ISOnum
-    'crarr':    0x21b5, # downwards arrow with corner leftwards = carriage return, U+21B5 NEW
-    'cup':      0x222a, # union = cup, U+222A ISOtech
-    'curren':   0x00a4, # currency sign, U+00A4 ISOnum
-    'dArr':     0x21d3, # downwards double arrow, U+21D3 ISOamsa
-    'dagger':   0x2020, # dagger, U+2020 ISOpub
-    'darr':     0x2193, # downwards arrow, U+2193 ISOnum
-    'deg':      0x00b0, # degree sign, U+00B0 ISOnum
-    'delta':    0x03b4, # greek small letter delta, U+03B4 ISOgrk3
-    'diams':    0x2666, # black diamond suit, U+2666 ISOpub
-    'divide':   0x00f7, # division sign, U+00F7 ISOnum
-    'eacute':   0x00e9, # latin small letter e with acute, U+00E9 ISOlat1
-    'ecirc':    0x00ea, # latin small letter e with circumflex, U+00EA ISOlat1
-    'egrave':   0x00e8, # latin small letter e with grave, U+00E8 ISOlat1
-    'empty':    0x2205, # empty set = null set = diameter, U+2205 ISOamso
-    'emsp':     0x2003, # em space, U+2003 ISOpub
-    'ensp':     0x2002, # en space, U+2002 ISOpub
-    'epsilon':  0x03b5, # greek small letter epsilon, U+03B5 ISOgrk3
-    'equiv':    0x2261, # identical to, U+2261 ISOtech
-    'eta':      0x03b7, # greek small letter eta, U+03B7 ISOgrk3
-    'eth':      0x00f0, # latin small letter eth, U+00F0 ISOlat1
-    'euml':     0x00eb, # latin small letter e with diaeresis, U+00EB ISOlat1
-    'euro':     0x20ac, # euro sign, U+20AC NEW
-    'exist':    0x2203, # there exists, U+2203 ISOtech
-    'fnof':     0x0192, # latin small f with hook = function = florin, U+0192 ISOtech
-    'forall':   0x2200, # for all, U+2200 ISOtech
-    'frac12':   0x00bd, # vulgar fraction one half = fraction one half, U+00BD ISOnum
-    'frac14':   0x00bc, # vulgar fraction one quarter = fraction one quarter, U+00BC ISOnum
-    'frac34':   0x00be, # vulgar fraction three quarters = fraction three quarters, U+00BE ISOnum
-    'frasl':    0x2044, # fraction slash, U+2044 NEW
-    'gamma':    0x03b3, # greek small letter gamma, U+03B3 ISOgrk3
-    'ge':       0x2265, # greater-than or equal to, U+2265 ISOtech
-    'gt':       0x003e, # greater-than sign, U+003E ISOnum
-    'hArr':     0x21d4, # left right double arrow, U+21D4 ISOamsa
-    'harr':     0x2194, # left right arrow, U+2194 ISOamsa
-    'hearts':   0x2665, # black heart suit = valentine, U+2665 ISOpub
-    'hellip':   0x2026, # horizontal ellipsis = three dot leader, U+2026 ISOpub
-    'iacute':   0x00ed, # latin small letter i with acute, U+00ED ISOlat1
-    'icirc':    0x00ee, # latin small letter i with circumflex, U+00EE ISOlat1
-    'iexcl':    0x00a1, # inverted exclamation mark, U+00A1 ISOnum
-    'igrave':   0x00ec, # latin small letter i with grave, U+00EC ISOlat1
-    'image':    0x2111, # blackletter capital I = imaginary part, U+2111 ISOamso
-    'infin':    0x221e, # infinity, U+221E ISOtech
-    'int':      0x222b, # integral, U+222B ISOtech
-    'iota':     0x03b9, # greek small letter iota, U+03B9 ISOgrk3
-    'iquest':   0x00bf, # inverted question mark = turned question mark, U+00BF ISOnum
-    'isin':     0x2208, # element of, U+2208 ISOtech
-    'iuml':     0x00ef, # latin small letter i with diaeresis, U+00EF ISOlat1
-    'kappa':    0x03ba, # greek small letter kappa, U+03BA ISOgrk3
-    'lArr':     0x21d0, # leftwards double arrow, U+21D0 ISOtech
-    'lambda':   0x03bb, # greek small letter lambda, U+03BB ISOgrk3
-    'lang':     0x2329, # left-pointing angle bracket = bra, U+2329 ISOtech
-    'laquo':    0x00ab, # left-pointing double angle quotation mark = left pointing guillemet, U+00AB ISOnum
-    'larr':     0x2190, # leftwards arrow, U+2190 ISOnum
-    'lceil':    0x2308, # left ceiling = apl upstile, U+2308 ISOamsc
-    'ldquo':    0x201c, # left double quotation mark, U+201C ISOnum
-    'le':       0x2264, # less-than or equal to, U+2264 ISOtech
-    'lfloor':   0x230a, # left floor = apl downstile, U+230A ISOamsc
-    'lowast':   0x2217, # asterisk operator, U+2217 ISOtech
-    'loz':      0x25ca, # lozenge, U+25CA ISOpub
-    'lrm':      0x200e, # left-to-right mark, U+200E NEW RFC 2070
-    'lsaquo':   0x2039, # single left-pointing angle quotation mark, U+2039 ISO proposed
-    'lsquo':    0x2018, # left single quotation mark, U+2018 ISOnum
-    'lt':       0x003c, # less-than sign, U+003C ISOnum
-    'macr':     0x00af, # macron = spacing macron = overline = APL overbar, U+00AF ISOdia
-    'mdash':    0x2014, # em dash, U+2014 ISOpub
-    'micro':    0x00b5, # micro sign, U+00B5 ISOnum
-    'middot':   0x00b7, # middle dot = Georgian comma = Greek middle dot, U+00B7 ISOnum
-    'minus':    0x2212, # minus sign, U+2212 ISOtech
-    'mu':       0x03bc, # greek small letter mu, U+03BC ISOgrk3
-    'nabla':    0x2207, # nabla = backward difference, U+2207 ISOtech
-    'nbsp':     0x00a0, # no-break space = non-breaking space, U+00A0 ISOnum
-    'ndash':    0x2013, # en dash, U+2013 ISOpub
-    'ne':       0x2260, # not equal to, U+2260 ISOtech
-    'ni':       0x220b, # contains as member, U+220B ISOtech
-    'not':      0x00ac, # not sign, U+00AC ISOnum
-    'notin':    0x2209, # not an element of, U+2209 ISOtech
-    'nsub':     0x2284, # not a subset of, U+2284 ISOamsn
-    'ntilde':   0x00f1, # latin small letter n with tilde, U+00F1 ISOlat1
-    'nu':       0x03bd, # greek small letter nu, U+03BD ISOgrk3
-    'oacute':   0x00f3, # latin small letter o with acute, U+00F3 ISOlat1
-    'ocirc':    0x00f4, # latin small letter o with circumflex, U+00F4 ISOlat1
-    'oelig':    0x0153, # latin small ligature oe, U+0153 ISOlat2
-    'ograve':   0x00f2, # latin small letter o with grave, U+00F2 ISOlat1
-    'oline':    0x203e, # overline = spacing overscore, U+203E NEW
-    'omega':    0x03c9, # greek small letter omega, U+03C9 ISOgrk3
-    'omicron':  0x03bf, # greek small letter omicron, U+03BF NEW
-    'oplus':    0x2295, # circled plus = direct sum, U+2295 ISOamsb
-    'or':       0x2228, # logical or = vee, U+2228 ISOtech
-    'ordf':     0x00aa, # feminine ordinal indicator, U+00AA ISOnum
-    'ordm':     0x00ba, # masculine ordinal indicator, U+00BA ISOnum
-    'oslash':   0x00f8, # latin small letter o with stroke, = latin small letter o slash, U+00F8 ISOlat1
-    'otilde':   0x00f5, # latin small letter o with tilde, U+00F5 ISOlat1
-    'otimes':   0x2297, # circled times = vector product, U+2297 ISOamsb
-    'ouml':     0x00f6, # latin small letter o with diaeresis, U+00F6 ISOlat1
-    'para':     0x00b6, # pilcrow sign = paragraph sign, U+00B6 ISOnum
-    'part':     0x2202, # partial differential, U+2202 ISOtech
-    'permil':   0x2030, # per mille sign, U+2030 ISOtech
-    'perp':     0x22a5, # up tack = orthogonal to = perpendicular, U+22A5 ISOtech
-    'phi':      0x03c6, # greek small letter phi, U+03C6 ISOgrk3
-    'pi':       0x03c0, # greek small letter pi, U+03C0 ISOgrk3
-    'piv':      0x03d6, # greek pi symbol, U+03D6 ISOgrk3
-    'plusmn':   0x00b1, # plus-minus sign = plus-or-minus sign, U+00B1 ISOnum
-    'pound':    0x00a3, # pound sign, U+00A3 ISOnum
-    'prime':    0x2032, # prime = minutes = feet, U+2032 ISOtech
-    'prod':     0x220f, # n-ary product = product sign, U+220F ISOamsb
-    'prop':     0x221d, # proportional to, U+221D ISOtech
-    'psi':      0x03c8, # greek small letter psi, U+03C8 ISOgrk3
-    'quot':     0x0022, # quotation mark = APL quote, U+0022 ISOnum
-    'rArr':     0x21d2, # rightwards double arrow, U+21D2 ISOtech
-    'radic':    0x221a, # square root = radical sign, U+221A ISOtech
-    'rang':     0x232a, # right-pointing angle bracket = ket, U+232A ISOtech
-    'raquo':    0x00bb, # right-pointing double angle quotation mark = right pointing guillemet, U+00BB ISOnum
-    'rarr':     0x2192, # rightwards arrow, U+2192 ISOnum
-    'rceil':    0x2309, # right ceiling, U+2309 ISOamsc
-    'rdquo':    0x201d, # right double quotation mark, U+201D ISOnum
-    'real':     0x211c, # blackletter capital R = real part symbol, U+211C ISOamso
-    'reg':      0x00ae, # registered sign = registered trade mark sign, U+00AE ISOnum
-    'rfloor':   0x230b, # right floor, U+230B ISOamsc
-    'rho':      0x03c1, # greek small letter rho, U+03C1 ISOgrk3
-    'rlm':      0x200f, # right-to-left mark, U+200F NEW RFC 2070
-    'rsaquo':   0x203a, # single right-pointing angle quotation mark, U+203A ISO proposed
-    'rsquo':    0x2019, # right single quotation mark, U+2019 ISOnum
-    'sbquo':    0x201a, # single low-9 quotation mark, U+201A NEW
-    'scaron':   0x0161, # latin small letter s with caron, U+0161 ISOlat2
-    'sdot':     0x22c5, # dot operator, U+22C5 ISOamsb
-    'sect':     0x00a7, # section sign, U+00A7 ISOnum
-    'shy':      0x00ad, # soft hyphen = discretionary hyphen, U+00AD ISOnum
-    'sigma':    0x03c3, # greek small letter sigma, U+03C3 ISOgrk3
-    'sigmaf':   0x03c2, # greek small letter final sigma, U+03C2 ISOgrk3
-    'sim':      0x223c, # tilde operator = varies with = similar to, U+223C ISOtech
-    'spades':   0x2660, # black spade suit, U+2660 ISOpub
-    'sub':      0x2282, # subset of, U+2282 ISOtech
-    'sube':     0x2286, # subset of or equal to, U+2286 ISOtech
-    'sum':      0x2211, # n-ary sumation, U+2211 ISOamsb
-    'sup':      0x2283, # superset of, U+2283 ISOtech
-    'sup1':     0x00b9, # superscript one = superscript digit one, U+00B9 ISOnum
-    'sup2':     0x00b2, # superscript two = superscript digit two = squared, U+00B2 ISOnum
-    'sup3':     0x00b3, # superscript three = superscript digit three = cubed, U+00B3 ISOnum
-    'supe':     0x2287, # superset of or equal to, U+2287 ISOtech
-    'szlig':    0x00df, # latin small letter sharp s = ess-zed, U+00DF ISOlat1
-    'tau':      0x03c4, # greek small letter tau, U+03C4 ISOgrk3
-    'there4':   0x2234, # therefore, U+2234 ISOtech
-    'theta':    0x03b8, # greek small letter theta, U+03B8 ISOgrk3
-    'thetasym': 0x03d1, # greek small letter theta symbol, U+03D1 NEW
-    'thinsp':   0x2009, # thin space, U+2009 ISOpub
-    'thorn':    0x00fe, # latin small letter thorn with, U+00FE ISOlat1
-    'tilde':    0x02dc, # small tilde, U+02DC ISOdia
-    'times':    0x00d7, # multiplication sign, U+00D7 ISOnum
-    'trade':    0x2122, # trade mark sign, U+2122 ISOnum
-    'uArr':     0x21d1, # upwards double arrow, U+21D1 ISOamsa
-    'uacute':   0x00fa, # latin small letter u with acute, U+00FA ISOlat1
-    'uarr':     0x2191, # upwards arrow, U+2191 ISOnum
-    'ucirc':    0x00fb, # latin small letter u with circumflex, U+00FB ISOlat1
-    'ugrave':   0x00f9, # latin small letter u with grave, U+00F9 ISOlat1
-    'uml':      0x00a8, # diaeresis = spacing diaeresis, U+00A8 ISOdia
-    'upsih':    0x03d2, # greek upsilon with hook symbol, U+03D2 NEW
-    'upsilon':  0x03c5, # greek small letter upsilon, U+03C5 ISOgrk3
-    'uuml':     0x00fc, # latin small letter u with diaeresis, U+00FC ISOlat1
-    'weierp':   0x2118, # script capital P = power set = Weierstrass p, U+2118 ISOamso
-    'xi':       0x03be, # greek small letter xi, U+03BE ISOgrk3
-    'yacute':   0x00fd, # latin small letter y with acute, U+00FD ISOlat1
-    'yen':      0x00a5, # yen sign = yuan sign, U+00A5 ISOnum
-    'yuml':     0x00ff, # latin small letter y with diaeresis, U+00FF ISOlat1
-    'zeta':     0x03b6, # greek small letter zeta, U+03B6 ISOgrk3
-    'zwj':      0x200d, # zero width joiner, U+200D NEW RFC 2070
-    'zwnj':     0x200c, # zero width non-joiner, U+200C NEW RFC 2070
-}
-
-
-# maps the HTML5 named character references to the equivalent Unicode character(s)
-html5 = {
-    'Aacute': '\xc1',
-    'aacute': '\xe1',
-    'Aacute;': '\xc1',
-    'aacute;': '\xe1',
-    'Abreve;': '\u0102',
-    'abreve;': '\u0103',
-    'ac;': '\u223e',
-    'acd;': '\u223f',
-    'acE;': '\u223e\u0333',
-    'Acirc': '\xc2',
-    'acirc': '\xe2',
-    'Acirc;': '\xc2',
-    'acirc;': '\xe2',
-    'acute': '\xb4',
-    'acute;': '\xb4',
-    'Acy;': '\u0410',
-    'acy;': '\u0430',
-    'AElig': '\xc6',
-    'aelig': '\xe6',
-    'AElig;': '\xc6',
-    'aelig;': '\xe6',
-    'af;': '\u2061',
-    'Afr;': '\U0001d504',
-    'afr;': '\U0001d51e',
-    'Agrave': '\xc0',
-    'agrave': '\xe0',
-    'Agrave;': '\xc0',
-    'agrave;': '\xe0',
-    'alefsym;': '\u2135',
-    'aleph;': '\u2135',
-    'Alpha;': '\u0391',
-    'alpha;': '\u03b1',
-    'Amacr;': '\u0100',
-    'amacr;': '\u0101',
-    'amalg;': '\u2a3f',
-    'AMP': '&',
-    'amp': '&',
-    'AMP;': '&',
-    'amp;': '&',
-    'And;': '\u2a53',
-    'and;': '\u2227',
-    'andand;': '\u2a55',
-    'andd;': '\u2a5c',
-    'andslope;': '\u2a58',
-    'andv;': '\u2a5a',
-    'ang;': '\u2220',
-    'ange;': '\u29a4',
-    'angle;': '\u2220',
-    'angmsd;': '\u2221',
-    'angmsdaa;': '\u29a8',
-    'angmsdab;': '\u29a9',
-    'angmsdac;': '\u29aa',
-    'angmsdad;': '\u29ab',
-    'angmsdae;': '\u29ac',
-    'angmsdaf;': '\u29ad',
-    'angmsdag;': '\u29ae',
-    'angmsdah;': '\u29af',
-    'angrt;': '\u221f',
-    'angrtvb;': '\u22be',
-    'angrtvbd;': '\u299d',
-    'angsph;': '\u2222',
-    'angst;': '\xc5',
-    'angzarr;': '\u237c',
-    'Aogon;': '\u0104',
-    'aogon;': '\u0105',
-    'Aopf;': '\U0001d538',
-    'aopf;': '\U0001d552',
-    'ap;': '\u2248',
-    'apacir;': '\u2a6f',
-    'apE;': '\u2a70',
-    'ape;': '\u224a',
-    'apid;': '\u224b',
-    'apos;': "'",
-    'ApplyFunction;': '\u2061',
-    'approx;': '\u2248',
-    'approxeq;': '\u224a',
-    'Aring': '\xc5',
-    'aring': '\xe5',
-    'Aring;': '\xc5',
-    'aring;': '\xe5',
-    'Ascr;': '\U0001d49c',
-    'ascr;': '\U0001d4b6',
-    'Assign;': '\u2254',
-    'ast;': '*',
-    'asymp;': '\u2248',
-    'asympeq;': '\u224d',
-    'Atilde': '\xc3',
-    'atilde': '\xe3',
-    'Atilde;': '\xc3',
-    'atilde;': '\xe3',
-    'Auml': '\xc4',
-    'auml': '\xe4',
-    'Auml;': '\xc4',
-    'auml;': '\xe4',
-    'awconint;': '\u2233',
-    'awint;': '\u2a11',
-    'backcong;': '\u224c',
-    'backepsilon;': '\u03f6',
-    'backprime;': '\u2035',
-    'backsim;': '\u223d',
-    'backsimeq;': '\u22cd',
-    'Backslash;': '\u2216',
-    'Barv;': '\u2ae7',
-    'barvee;': '\u22bd',
-    'Barwed;': '\u2306',
-    'barwed;': '\u2305',
-    'barwedge;': '\u2305',
-    'bbrk;': '\u23b5',
-    'bbrktbrk;': '\u23b6',
-    'bcong;': '\u224c',
-    'Bcy;': '\u0411',
-    'bcy;': '\u0431',
-    'bdquo;': '\u201e',
-    'becaus;': '\u2235',
-    'Because;': '\u2235',
-    'because;': '\u2235',
-    'bemptyv;': '\u29b0',
-    'bepsi;': '\u03f6',
-    'bernou;': '\u212c',
-    'Bernoullis;': '\u212c',
-    'Beta;': '\u0392',
-    'beta;': '\u03b2',
-    'beth;': '\u2136',
-    'between;': '\u226c',
-    'Bfr;': '\U0001d505',
-    'bfr;': '\U0001d51f',
-    'bigcap;': '\u22c2',
-    'bigcirc;': '\u25ef',
-    'bigcup;': '\u22c3',
-    'bigodot;': '\u2a00',
-    'bigoplus;': '\u2a01',
-    'bigotimes;': '\u2a02',
-    'bigsqcup;': '\u2a06',
-    'bigstar;': '\u2605',
-    'bigtriangledown;': '\u25bd',
-    'bigtriangleup;': '\u25b3',
-    'biguplus;': '\u2a04',
-    'bigvee;': '\u22c1',
-    'bigwedge;': '\u22c0',
-    'bkarow;': '\u290d',
-    'blacklozenge;': '\u29eb',
-    'blacksquare;': '\u25aa',
-    'blacktriangle;': '\u25b4',
-    'blacktriangledown;': '\u25be',
-    'blacktriangleleft;': '\u25c2',
-    'blacktriangleright;': '\u25b8',
-    'blank;': '\u2423',
-    'blk12;': '\u2592',
-    'blk14;': '\u2591',
-    'blk34;': '\u2593',
-    'block;': '\u2588',
-    'bne;': '=\u20e5',
-    'bnequiv;': '\u2261\u20e5',
-    'bNot;': '\u2aed',
-    'bnot;': '\u2310',
-    'Bopf;': '\U0001d539',
-    'bopf;': '\U0001d553',
-    'bot;': '\u22a5',
-    'bottom;': '\u22a5',
-    'bowtie;': '\u22c8',
-    'boxbox;': '\u29c9',
-    'boxDL;': '\u2557',
-    'boxDl;': '\u2556',
-    'boxdL;': '\u2555',
-    'boxdl;': '\u2510',
-    'boxDR;': '\u2554',
-    'boxDr;': '\u2553',
-    'boxdR;': '\u2552',
-    'boxdr;': '\u250c',
-    'boxH;': '\u2550',
-    'boxh;': '\u2500',
-    'boxHD;': '\u2566',
-    'boxHd;': '\u2564',
-    'boxhD;': '\u2565',
-    'boxhd;': '\u252c',
-    'boxHU;': '\u2569',
-    'boxHu;': '\u2567',
-    'boxhU;': '\u2568',
-    'boxhu;': '\u2534',
-    'boxminus;': '\u229f',
-    'boxplus;': '\u229e',
-    'boxtimes;': '\u22a0',
-    'boxUL;': '\u255d',
-    'boxUl;': '\u255c',
-    'boxuL;': '\u255b',
-    'boxul;': '\u2518',
-    'boxUR;': '\u255a',
-    'boxUr;': '\u2559',
-    'boxuR;': '\u2558',
-    'boxur;': '\u2514',
-    'boxV;': '\u2551',
-    'boxv;': '\u2502',
-    'boxVH;': '\u256c',
-    'boxVh;': '\u256b',
-    'boxvH;': '\u256a',
-    'boxvh;': '\u253c',
-    'boxVL;': '\u2563',
-    'boxVl;': '\u2562',
-    'boxvL;': '\u2561',
-    'boxvl;': '\u2524',
-    'boxVR;': '\u2560',
-    'boxVr;': '\u255f',
-    'boxvR;': '\u255e',
-    'boxvr;': '\u251c',
-    'bprime;': '\u2035',
-    'Breve;': '\u02d8',
-    'breve;': '\u02d8',
-    'brvbar': '\xa6',
-    'brvbar;': '\xa6',
-    'Bscr;': '\u212c',
-    'bscr;': '\U0001d4b7',
-    'bsemi;': '\u204f',
-    'bsim;': '\u223d',
-    'bsime;': '\u22cd',
-    'bsol;': '\\',
-    'bsolb;': '\u29c5',
-    'bsolhsub;': '\u27c8',
-    'bull;': '\u2022',
-    'bullet;': '\u2022',
-    'bump;': '\u224e',
-    'bumpE;': '\u2aae',
-    'bumpe;': '\u224f',
-    'Bumpeq;': '\u224e',
-    'bumpeq;': '\u224f',
-    'Cacute;': '\u0106',
-    'cacute;': '\u0107',
-    'Cap;': '\u22d2',
-    'cap;': '\u2229',
-    'capand;': '\u2a44',
-    'capbrcup;': '\u2a49',
-    'capcap;': '\u2a4b',
-    'capcup;': '\u2a47',
-    'capdot;': '\u2a40',
-    'CapitalDifferentialD;': '\u2145',
-    'caps;': '\u2229\ufe00',
-    'caret;': '\u2041',
-    'caron;': '\u02c7',
-    'Cayleys;': '\u212d',
-    'ccaps;': '\u2a4d',
-    'Ccaron;': '\u010c',
-    'ccaron;': '\u010d',
-    'Ccedil': '\xc7',
-    'ccedil': '\xe7',
-    'Ccedil;': '\xc7',
-    'ccedil;': '\xe7',
-    'Ccirc;': '\u0108',
-    'ccirc;': '\u0109',
-    'Cconint;': '\u2230',
-    'ccups;': '\u2a4c',
-    'ccupssm;': '\u2a50',
-    'Cdot;': '\u010a',
-    'cdot;': '\u010b',
-    'cedil': '\xb8',
-    'cedil;': '\xb8',
-    'Cedilla;': '\xb8',
-    'cemptyv;': '\u29b2',
-    'cent': '\xa2',
-    'cent;': '\xa2',
-    'CenterDot;': '\xb7',
-    'centerdot;': '\xb7',
-    'Cfr;': '\u212d',
-    'cfr;': '\U0001d520',
-    'CHcy;': '\u0427',
-    'chcy;': '\u0447',
-    'check;': '\u2713',
-    'checkmark;': '\u2713',
-    'Chi;': '\u03a7',
-    'chi;': '\u03c7',
-    'cir;': '\u25cb',
-    'circ;': '\u02c6',
-    'circeq;': '\u2257',
-    'circlearrowleft;': '\u21ba',
-    'circlearrowright;': '\u21bb',
-    'circledast;': '\u229b',
-    'circledcirc;': '\u229a',
-    'circleddash;': '\u229d',
-    'CircleDot;': '\u2299',
-    'circledR;': '\xae',
-    'circledS;': '\u24c8',
-    'CircleMinus;': '\u2296',
-    'CirclePlus;': '\u2295',
-    'CircleTimes;': '\u2297',
-    'cirE;': '\u29c3',
-    'cire;': '\u2257',
-    'cirfnint;': '\u2a10',
-    'cirmid;': '\u2aef',
-    'cirscir;': '\u29c2',
-    'ClockwiseContourIntegral;': '\u2232',
-    'CloseCurlyDoubleQuote;': '\u201d',
-    'CloseCurlyQuote;': '\u2019',
-    'clubs;': '\u2663',
-    'clubsuit;': '\u2663',
-    'Colon;': '\u2237',
-    'colon;': ':',
-    'Colone;': '\u2a74',
-    'colone;': '\u2254',
-    'coloneq;': '\u2254',
-    'comma;': ',',
-    'commat;': '@',
-    'comp;': '\u2201',
-    'compfn;': '\u2218',
-    'complement;': '\u2201',
-    'complexes;': '\u2102',
-    'cong;': '\u2245',
-    'congdot;': '\u2a6d',
-    'Congruent;': '\u2261',
-    'Conint;': '\u222f',
-    'conint;': '\u222e',
-    'ContourIntegral;': '\u222e',
-    'Copf;': '\u2102',
-    'copf;': '\U0001d554',
-    'coprod;': '\u2210',
-    'Coproduct;': '\u2210',
-    'COPY': '\xa9',
-    'copy': '\xa9',
-    'COPY;': '\xa9',
-    'copy;': '\xa9',
-    'copysr;': '\u2117',
-    'CounterClockwiseContourIntegral;': '\u2233',
-    'crarr;': '\u21b5',
-    'Cross;': '\u2a2f',
-    'cross;': '\u2717',
-    'Cscr;': '\U0001d49e',
-    'cscr;': '\U0001d4b8',
-    'csub;': '\u2acf',
-    'csube;': '\u2ad1',
-    'csup;': '\u2ad0',
-    'csupe;': '\u2ad2',
-    'ctdot;': '\u22ef',
-    'cudarrl;': '\u2938',
-    'cudarrr;': '\u2935',
-    'cuepr;': '\u22de',
-    'cuesc;': '\u22df',
-    'cularr;': '\u21b6',
-    'cularrp;': '\u293d',
-    'Cup;': '\u22d3',
-    'cup;': '\u222a',
-    'cupbrcap;': '\u2a48',
-    'CupCap;': '\u224d',
-    'cupcap;': '\u2a46',
-    'cupcup;': '\u2a4a',
-    'cupdot;': '\u228d',
-    'cupor;': '\u2a45',
-    'cups;': '\u222a\ufe00',
-    'curarr;': '\u21b7',
-    'curarrm;': '\u293c',
-    'curlyeqprec;': '\u22de',
-    'curlyeqsucc;': '\u22df',
-    'curlyvee;': '\u22ce',
-    'curlywedge;': '\u22cf',
-    'curren': '\xa4',
-    'curren;': '\xa4',
-    'curvearrowleft;': '\u21b6',
-    'curvearrowright;': '\u21b7',
-    'cuvee;': '\u22ce',
-    'cuwed;': '\u22cf',
-    'cwconint;': '\u2232',
-    'cwint;': '\u2231',
-    'cylcty;': '\u232d',
-    'Dagger;': '\u2021',
-    'dagger;': '\u2020',
-    'daleth;': '\u2138',
-    'Darr;': '\u21a1',
-    'dArr;': '\u21d3',
-    'darr;': '\u2193',
-    'dash;': '\u2010',
-    'Dashv;': '\u2ae4',
-    'dashv;': '\u22a3',
-    'dbkarow;': '\u290f',
-    'dblac;': '\u02dd',
-    'Dcaron;': '\u010e',
-    'dcaron;': '\u010f',
-    'Dcy;': '\u0414',
-    'dcy;': '\u0434',
-    'DD;': '\u2145',
-    'dd;': '\u2146',
-    'ddagger;': '\u2021',
-    'ddarr;': '\u21ca',
-    'DDotrahd;': '\u2911',
-    'ddotseq;': '\u2a77',
-    'deg': '\xb0',
-    'deg;': '\xb0',
-    'Del;': '\u2207',
-    'Delta;': '\u0394',
-    'delta;': '\u03b4',
-    'demptyv;': '\u29b1',
-    'dfisht;': '\u297f',
-    'Dfr;': '\U0001d507',
-    'dfr;': '\U0001d521',
-    'dHar;': '\u2965',
-    'dharl;': '\u21c3',
-    'dharr;': '\u21c2',
-    'DiacriticalAcute;': '\xb4',
-    'DiacriticalDot;': '\u02d9',
-    'DiacriticalDoubleAcute;': '\u02dd',
-    'DiacriticalGrave;': '`',
-    'DiacriticalTilde;': '\u02dc',
-    'diam;': '\u22c4',
-    'Diamond;': '\u22c4',
-    'diamond;': '\u22c4',
-    'diamondsuit;': '\u2666',
-    'diams;': '\u2666',
-    'die;': '\xa8',
-    'DifferentialD;': '\u2146',
-    'digamma;': '\u03dd',
-    'disin;': '\u22f2',
-    'div;': '\xf7',
-    'divide': '\xf7',
-    'divide;': '\xf7',
-    'divideontimes;': '\u22c7',
-    'divonx;': '\u22c7',
-    'DJcy;': '\u0402',
-    'djcy;': '\u0452',
-    'dlcorn;': '\u231e',
-    'dlcrop;': '\u230d',
-    'dollar;': '$',
-    'Dopf;': '\U0001d53b',
-    'dopf;': '\U0001d555',
-    'Dot;': '\xa8',
-    'dot;': '\u02d9',
-    'DotDot;': '\u20dc',
-    'doteq;': '\u2250',
-    'doteqdot;': '\u2251',
-    'DotEqual;': '\u2250',
-    'dotminus;': '\u2238',
-    'dotplus;': '\u2214',
-    'dotsquare;': '\u22a1',
-    'doublebarwedge;': '\u2306',
-    'DoubleContourIntegral;': '\u222f',
-    'DoubleDot;': '\xa8',
-    'DoubleDownArrow;': '\u21d3',
-    'DoubleLeftArrow;': '\u21d0',
-    'DoubleLeftRightArrow;': '\u21d4',
-    'DoubleLeftTee;': '\u2ae4',
-    'DoubleLongLeftArrow;': '\u27f8',
-    'DoubleLongLeftRightArrow;': '\u27fa',
-    'DoubleLongRightArrow;': '\u27f9',
-    'DoubleRightArrow;': '\u21d2',
-    'DoubleRightTee;': '\u22a8',
-    'DoubleUpArrow;': '\u21d1',
-    'DoubleUpDownArrow;': '\u21d5',
-    'DoubleVerticalBar;': '\u2225',
-    'DownArrow;': '\u2193',
-    'Downarrow;': '\u21d3',
-    'downarrow;': '\u2193',
-    'DownArrowBar;': '\u2913',
-    'DownArrowUpArrow;': '\u21f5',
-    'DownBreve;': '\u0311',
-    'downdownarrows;': '\u21ca',
-    'downharpoonleft;': '\u21c3',
-    'downharpoonright;': '\u21c2',
-    'DownLeftRightVector;': '\u2950',
-    'DownLeftTeeVector;': '\u295e',
-    'DownLeftVector;': '\u21bd',
-    'DownLeftVectorBar;': '\u2956',
-    'DownRightTeeVector;': '\u295f',
-    'DownRightVector;': '\u21c1',
-    'DownRightVectorBar;': '\u2957',
-    'DownTee;': '\u22a4',
-    'DownTeeArrow;': '\u21a7',
-    'drbkarow;': '\u2910',
-    'drcorn;': '\u231f',
-    'drcrop;': '\u230c',
-    'Dscr;': '\U0001d49f',
-    'dscr;': '\U0001d4b9',
-    'DScy;': '\u0405',
-    'dscy;': '\u0455',
-    'dsol;': '\u29f6',
-    'Dstrok;': '\u0110',
-    'dstrok;': '\u0111',
-    'dtdot;': '\u22f1',
-    'dtri;': '\u25bf',
-    'dtrif;': '\u25be',
-    'duarr;': '\u21f5',
-    'duhar;': '\u296f',
-    'dwangle;': '\u29a6',
-    'DZcy;': '\u040f',
-    'dzcy;': '\u045f',
-    'dzigrarr;': '\u27ff',
-    'Eacute': '\xc9',
-    'eacute': '\xe9',
-    'Eacute;': '\xc9',
-    'eacute;': '\xe9',
-    'easter;': '\u2a6e',
-    'Ecaron;': '\u011a',
-    'ecaron;': '\u011b',
-    'ecir;': '\u2256',
-    'Ecirc': '\xca',
-    'ecirc': '\xea',
-    'Ecirc;': '\xca',
-    'ecirc;': '\xea',
-    'ecolon;': '\u2255',
-    'Ecy;': '\u042d',
-    'ecy;': '\u044d',
-    'eDDot;': '\u2a77',
-    'Edot;': '\u0116',
-    'eDot;': '\u2251',
-    'edot;': '\u0117',
-    'ee;': '\u2147',
-    'efDot;': '\u2252',
-    'Efr;': '\U0001d508',
-    'efr;': '\U0001d522',
-    'eg;': '\u2a9a',
-    'Egrave': '\xc8',
-    'egrave': '\xe8',
-    'Egrave;': '\xc8',
-    'egrave;': '\xe8',
-    'egs;': '\u2a96',
-    'egsdot;': '\u2a98',
-    'el;': '\u2a99',
-    'Element;': '\u2208',
-    'elinters;': '\u23e7',
-    'ell;': '\u2113',
-    'els;': '\u2a95',
-    'elsdot;': '\u2a97',
-    'Emacr;': '\u0112',
-    'emacr;': '\u0113',
-    'empty;': '\u2205',
-    'emptyset;': '\u2205',
-    'EmptySmallSquare;': '\u25fb',
-    'emptyv;': '\u2205',
-    'EmptyVerySmallSquare;': '\u25ab',
-    'emsp13;': '\u2004',
-    'emsp14;': '\u2005',
-    'emsp;': '\u2003',
-    'ENG;': '\u014a',
-    'eng;': '\u014b',
-    'ensp;': '\u2002',
-    'Eogon;': '\u0118',
-    'eogon;': '\u0119',
-    'Eopf;': '\U0001d53c',
-    'eopf;': '\U0001d556',
-    'epar;': '\u22d5',
-    'eparsl;': '\u29e3',
-    'eplus;': '\u2a71',
-    'epsi;': '\u03b5',
-    'Epsilon;': '\u0395',
-    'epsilon;': '\u03b5',
-    'epsiv;': '\u03f5',
-    'eqcirc;': '\u2256',
-    'eqcolon;': '\u2255',
-    'eqsim;': '\u2242',
-    'eqslantgtr;': '\u2a96',
-    'eqslantless;': '\u2a95',
-    'Equal;': '\u2a75',
-    'equals;': '=',
-    'EqualTilde;': '\u2242',
-    'equest;': '\u225f',
-    'Equilibrium;': '\u21cc',
-    'equiv;': '\u2261',
-    'equivDD;': '\u2a78',
-    'eqvparsl;': '\u29e5',
-    'erarr;': '\u2971',
-    'erDot;': '\u2253',
-    'Escr;': '\u2130',
-    'escr;': '\u212f',
-    'esdot;': '\u2250',
-    'Esim;': '\u2a73',
-    'esim;': '\u2242',
-    'Eta;': '\u0397',
-    'eta;': '\u03b7',
-    'ETH': '\xd0',
-    'eth': '\xf0',
-    'ETH;': '\xd0',
-    'eth;': '\xf0',
-    'Euml': '\xcb',
-    'euml': '\xeb',
-    'Euml;': '\xcb',
-    'euml;': '\xeb',
-    'euro;': '\u20ac',
-    'excl;': '!',
-    'exist;': '\u2203',
-    'Exists;': '\u2203',
-    'expectation;': '\u2130',
-    'ExponentialE;': '\u2147',
-    'exponentiale;': '\u2147',
-    'fallingdotseq;': '\u2252',
-    'Fcy;': '\u0424',
-    'fcy;': '\u0444',
-    'female;': '\u2640',
-    'ffilig;': '\ufb03',
-    'fflig;': '\ufb00',
-    'ffllig;': '\ufb04',
-    'Ffr;': '\U0001d509',
-    'ffr;': '\U0001d523',
-    'filig;': '\ufb01',
-    'FilledSmallSquare;': '\u25fc',
-    'FilledVerySmallSquare;': '\u25aa',
-    'fjlig;': 'fj',
-    'flat;': '\u266d',
-    'fllig;': '\ufb02',
-    'fltns;': '\u25b1',
-    'fnof;': '\u0192',
-    'Fopf;': '\U0001d53d',
-    'fopf;': '\U0001d557',
-    'ForAll;': '\u2200',
-    'forall;': '\u2200',
-    'fork;': '\u22d4',
-    'forkv;': '\u2ad9',
-    'Fouriertrf;': '\u2131',
-    'fpartint;': '\u2a0d',
-    'frac12': '\xbd',
-    'frac12;': '\xbd',
-    'frac13;': '\u2153',
-    'frac14': '\xbc',
-    'frac14;': '\xbc',
-    'frac15;': '\u2155',
-    'frac16;': '\u2159',
-    'frac18;': '\u215b',
-    'frac23;': '\u2154',
-    'frac25;': '\u2156',
-    'frac34': '\xbe',
-    'frac34;': '\xbe',
-    'frac35;': '\u2157',
-    'frac38;': '\u215c',
-    'frac45;': '\u2158',
-    'frac56;': '\u215a',
-    'frac58;': '\u215d',
-    'frac78;': '\u215e',
-    'frasl;': '\u2044',
-    'frown;': '\u2322',
-    'Fscr;': '\u2131',
-    'fscr;': '\U0001d4bb',
-    'gacute;': '\u01f5',
-    'Gamma;': '\u0393',
-    'gamma;': '\u03b3',
-    'Gammad;': '\u03dc',
-    'gammad;': '\u03dd',
-    'gap;': '\u2a86',
-    'Gbreve;': '\u011e',
-    'gbreve;': '\u011f',
-    'Gcedil;': '\u0122',
-    'Gcirc;': '\u011c',
-    'gcirc;': '\u011d',
-    'Gcy;': '\u0413',
-    'gcy;': '\u0433',
-    'Gdot;': '\u0120',
-    'gdot;': '\u0121',
-    'gE;': '\u2267',
-    'ge;': '\u2265',
-    'gEl;': '\u2a8c',
-    'gel;': '\u22db',
-    'geq;': '\u2265',
-    'geqq;': '\u2267',
-    'geqslant;': '\u2a7e',
-    'ges;': '\u2a7e',
-    'gescc;': '\u2aa9',
-    'gesdot;': '\u2a80',
-    'gesdoto;': '\u2a82',
-    'gesdotol;': '\u2a84',
-    'gesl;': '\u22db\ufe00',
-    'gesles;': '\u2a94',
-    'Gfr;': '\U0001d50a',
-    'gfr;': '\U0001d524',
-    'Gg;': '\u22d9',
-    'gg;': '\u226b',
-    'ggg;': '\u22d9',
-    'gimel;': '\u2137',
-    'GJcy;': '\u0403',
-    'gjcy;': '\u0453',
-    'gl;': '\u2277',
-    'gla;': '\u2aa5',
-    'glE;': '\u2a92',
-    'glj;': '\u2aa4',
-    'gnap;': '\u2a8a',
-    'gnapprox;': '\u2a8a',
-    'gnE;': '\u2269',
-    'gne;': '\u2a88',
-    'gneq;': '\u2a88',
-    'gneqq;': '\u2269',
-    'gnsim;': '\u22e7',
-    'Gopf;': '\U0001d53e',
-    'gopf;': '\U0001d558',
-    'grave;': '`',
-    'GreaterEqual;': '\u2265',
-    'GreaterEqualLess;': '\u22db',
-    'GreaterFullEqual;': '\u2267',
-    'GreaterGreater;': '\u2aa2',
-    'GreaterLess;': '\u2277',
-    'GreaterSlantEqual;': '\u2a7e',
-    'GreaterTilde;': '\u2273',
-    'Gscr;': '\U0001d4a2',
-    'gscr;': '\u210a',
-    'gsim;': '\u2273',
-    'gsime;': '\u2a8e',
-    'gsiml;': '\u2a90',
-    'GT': '>',
-    'gt': '>',
-    'GT;': '>',
-    'Gt;': '\u226b',
-    'gt;': '>',
-    'gtcc;': '\u2aa7',
-    'gtcir;': '\u2a7a',
-    'gtdot;': '\u22d7',
-    'gtlPar;': '\u2995',
-    'gtquest;': '\u2a7c',
-    'gtrapprox;': '\u2a86',
-    'gtrarr;': '\u2978',
-    'gtrdot;': '\u22d7',
-    'gtreqless;': '\u22db',
-    'gtreqqless;': '\u2a8c',
-    'gtrless;': '\u2277',
-    'gtrsim;': '\u2273',
-    'gvertneqq;': '\u2269\ufe00',
-    'gvnE;': '\u2269\ufe00',
-    'Hacek;': '\u02c7',
-    'hairsp;': '\u200a',
-    'half;': '\xbd',
-    'hamilt;': '\u210b',
-    'HARDcy;': '\u042a',
-    'hardcy;': '\u044a',
-    'hArr;': '\u21d4',
-    'harr;': '\u2194',
-    'harrcir;': '\u2948',
-    'harrw;': '\u21ad',
-    'Hat;': '^',
-    'hbar;': '\u210f',
-    'Hcirc;': '\u0124',
-    'hcirc;': '\u0125',
-    'hearts;': '\u2665',
-    'heartsuit;': '\u2665',
-    'hellip;': '\u2026',
-    'hercon;': '\u22b9',
-    'Hfr;': '\u210c',
-    'hfr;': '\U0001d525',
-    'HilbertSpace;': '\u210b',
-    'hksearow;': '\u2925',
-    'hkswarow;': '\u2926',
-    'hoarr;': '\u21ff',
-    'homtht;': '\u223b',
-    'hookleftarrow;': '\u21a9',
-    'hookrightarrow;': '\u21aa',
-    'Hopf;': '\u210d',
-    'hopf;': '\U0001d559',
-    'horbar;': '\u2015',
-    'HorizontalLine;': '\u2500',
-    'Hscr;': '\u210b',
-    'hscr;': '\U0001d4bd',
-    'hslash;': '\u210f',
-    'Hstrok;': '\u0126',
-    'hstrok;': '\u0127',
-    'HumpDownHump;': '\u224e',
-    'HumpEqual;': '\u224f',
-    'hybull;': '\u2043',
-    'hyphen;': '\u2010',
-    'Iacute': '\xcd',
-    'iacute': '\xed',
-    'Iacute;': '\xcd',
-    'iacute;': '\xed',
-    'ic;': '\u2063',
-    'Icirc': '\xce',
-    'icirc': '\xee',
-    'Icirc;': '\xce',
-    'icirc;': '\xee',
-    'Icy;': '\u0418',
-    'icy;': '\u0438',
-    'Idot;': '\u0130',
-    'IEcy;': '\u0415',
-    'iecy;': '\u0435',
-    'iexcl': '\xa1',
-    'iexcl;': '\xa1',
-    'iff;': '\u21d4',
-    'Ifr;': '\u2111',
-    'ifr;': '\U0001d526',
-    'Igrave': '\xcc',
-    'igrave': '\xec',
-    'Igrave;': '\xcc',
-    'igrave;': '\xec',
-    'ii;': '\u2148',
-    'iiiint;': '\u2a0c',
-    'iiint;': '\u222d',
-    'iinfin;': '\u29dc',
-    'iiota;': '\u2129',
-    'IJlig;': '\u0132',
-    'ijlig;': '\u0133',
-    'Im;': '\u2111',
-    'Imacr;': '\u012a',
-    'imacr;': '\u012b',
-    'image;': '\u2111',
-    'ImaginaryI;': '\u2148',
-    'imagline;': '\u2110',
-    'imagpart;': '\u2111',
-    'imath;': '\u0131',
-    'imof;': '\u22b7',
-    'imped;': '\u01b5',
-    'Implies;': '\u21d2',
-    'in;': '\u2208',
-    'incare;': '\u2105',
-    'infin;': '\u221e',
-    'infintie;': '\u29dd',
-    'inodot;': '\u0131',
-    'Int;': '\u222c',
-    'int;': '\u222b',
-    'intcal;': '\u22ba',
-    'integers;': '\u2124',
-    'Integral;': '\u222b',
-    'intercal;': '\u22ba',
-    'Intersection;': '\u22c2',
-    'intlarhk;': '\u2a17',
-    'intprod;': '\u2a3c',
-    'InvisibleComma;': '\u2063',
-    'InvisibleTimes;': '\u2062',
-    'IOcy;': '\u0401',
-    'iocy;': '\u0451',
-    'Iogon;': '\u012e',
-    'iogon;': '\u012f',
-    'Iopf;': '\U0001d540',
-    'iopf;': '\U0001d55a',
-    'Iota;': '\u0399',
-    'iota;': '\u03b9',
-    'iprod;': '\u2a3c',
-    'iquest': '\xbf',
-    'iquest;': '\xbf',
-    'Iscr;': '\u2110',
-    'iscr;': '\U0001d4be',
-    'isin;': '\u2208',
-    'isindot;': '\u22f5',
-    'isinE;': '\u22f9',
-    'isins;': '\u22f4',
-    'isinsv;': '\u22f3',
-    'isinv;': '\u2208',
-    'it;': '\u2062',
-    'Itilde;': '\u0128',
-    'itilde;': '\u0129',
-    'Iukcy;': '\u0406',
-    'iukcy;': '\u0456',
-    'Iuml': '\xcf',
-    'iuml': '\xef',
-    'Iuml;': '\xcf',
-    'iuml;': '\xef',
-    'Jcirc;': '\u0134',
-    'jcirc;': '\u0135',
-    'Jcy;': '\u0419',
-    'jcy;': '\u0439',
-    'Jfr;': '\U0001d50d',
-    'jfr;': '\U0001d527',
-    'jmath;': '\u0237',
-    'Jopf;': '\U0001d541',
-    'jopf;': '\U0001d55b',
-    'Jscr;': '\U0001d4a5',
-    'jscr;': '\U0001d4bf',
-    'Jsercy;': '\u0408',
-    'jsercy;': '\u0458',
-    'Jukcy;': '\u0404',
-    'jukcy;': '\u0454',
-    'Kappa;': '\u039a',
-    'kappa;': '\u03ba',
-    'kappav;': '\u03f0',
-    'Kcedil;': '\u0136',
-    'kcedil;': '\u0137',
-    'Kcy;': '\u041a',
-    'kcy;': '\u043a',
-    'Kfr;': '\U0001d50e',
-    'kfr;': '\U0001d528',
-    'kgreen;': '\u0138',
-    'KHcy;': '\u0425',
-    'khcy;': '\u0445',
-    'KJcy;': '\u040c',
-    'kjcy;': '\u045c',
-    'Kopf;': '\U0001d542',
-    'kopf;': '\U0001d55c',
-    'Kscr;': '\U0001d4a6',
-    'kscr;': '\U0001d4c0',
-    'lAarr;': '\u21da',
-    'Lacute;': '\u0139',
-    'lacute;': '\u013a',
-    'laemptyv;': '\u29b4',
-    'lagran;': '\u2112',
-    'Lambda;': '\u039b',
-    'lambda;': '\u03bb',
-    'Lang;': '\u27ea',
-    'lang;': '\u27e8',
-    'langd;': '\u2991',
-    'langle;': '\u27e8',
-    'lap;': '\u2a85',
-    'Laplacetrf;': '\u2112',
-    'laquo': '\xab',
-    'laquo;': '\xab',
-    'Larr;': '\u219e',
-    'lArr;': '\u21d0',
-    'larr;': '\u2190',
-    'larrb;': '\u21e4',
-    'larrbfs;': '\u291f',
-    'larrfs;': '\u291d',
-    'larrhk;': '\u21a9',
-    'larrlp;': '\u21ab',
-    'larrpl;': '\u2939',
-    'larrsim;': '\u2973',
-    'larrtl;': '\u21a2',
-    'lat;': '\u2aab',
-    'lAtail;': '\u291b',
-    'latail;': '\u2919',
-    'late;': '\u2aad',
-    'lates;': '\u2aad\ufe00',
-    'lBarr;': '\u290e',
-    'lbarr;': '\u290c',
-    'lbbrk;': '\u2772',
-    'lbrace;': '{',
-    'lbrack;': '[',
-    'lbrke;': '\u298b',
-    'lbrksld;': '\u298f',
-    'lbrkslu;': '\u298d',
-    'Lcaron;': '\u013d',
-    'lcaron;': '\u013e',
-    'Lcedil;': '\u013b',
-    'lcedil;': '\u013c',
-    'lceil;': '\u2308',
-    'lcub;': '{',
-    'Lcy;': '\u041b',
-    'lcy;': '\u043b',
-    'ldca;': '\u2936',
-    'ldquo;': '\u201c',
-    'ldquor;': '\u201e',
-    'ldrdhar;': '\u2967',
-    'ldrushar;': '\u294b',
-    'ldsh;': '\u21b2',
-    'lE;': '\u2266',
-    'le;': '\u2264',
-    'LeftAngleBracket;': '\u27e8',
-    'LeftArrow;': '\u2190',
-    'Leftarrow;': '\u21d0',
-    'leftarrow;': '\u2190',
-    'LeftArrowBar;': '\u21e4',
-    'LeftArrowRightArrow;': '\u21c6',
-    'leftarrowtail;': '\u21a2',
-    'LeftCeiling;': '\u2308',
-    'LeftDoubleBracket;': '\u27e6',
-    'LeftDownTeeVector;': '\u2961',
-    'LeftDownVector;': '\u21c3',
-    'LeftDownVectorBar;': '\u2959',
-    'LeftFloor;': '\u230a',
-    'leftharpoondown;': '\u21bd',
-    'leftharpoonup;': '\u21bc',
-    'leftleftarrows;': '\u21c7',
-    'LeftRightArrow;': '\u2194',
-    'Leftrightarrow;': '\u21d4',
-    'leftrightarrow;': '\u2194',
-    'leftrightarrows;': '\u21c6',
-    'leftrightharpoons;': '\u21cb',
-    'leftrightsquigarrow;': '\u21ad',
-    'LeftRightVector;': '\u294e',
-    'LeftTee;': '\u22a3',
-    'LeftTeeArrow;': '\u21a4',
-    'LeftTeeVector;': '\u295a',
-    'leftthreetimes;': '\u22cb',
-    'LeftTriangle;': '\u22b2',
-    'LeftTriangleBar;': '\u29cf',
-    'LeftTriangleEqual;': '\u22b4',
-    'LeftUpDownVector;': '\u2951',
-    'LeftUpTeeVector;': '\u2960',
-    'LeftUpVector;': '\u21bf',
-    'LeftUpVectorBar;': '\u2958',
-    'LeftVector;': '\u21bc',
-    'LeftVectorBar;': '\u2952',
-    'lEg;': '\u2a8b',
-    'leg;': '\u22da',
-    'leq;': '\u2264',
-    'leqq;': '\u2266',
-    'leqslant;': '\u2a7d',
-    'les;': '\u2a7d',
-    'lescc;': '\u2aa8',
-    'lesdot;': '\u2a7f',
-    'lesdoto;': '\u2a81',
-    'lesdotor;': '\u2a83',
-    'lesg;': '\u22da\ufe00',
-    'lesges;': '\u2a93',
-    'lessapprox;': '\u2a85',
-    'lessdot;': '\u22d6',
-    'lesseqgtr;': '\u22da',
-    'lesseqqgtr;': '\u2a8b',
-    'LessEqualGreater;': '\u22da',
-    'LessFullEqual;': '\u2266',
-    'LessGreater;': '\u2276',
-    'lessgtr;': '\u2276',
-    'LessLess;': '\u2aa1',
-    'lesssim;': '\u2272',
-    'LessSlantEqual;': '\u2a7d',
-    'LessTilde;': '\u2272',
-    'lfisht;': '\u297c',
-    'lfloor;': '\u230a',
-    'Lfr;': '\U0001d50f',
-    'lfr;': '\U0001d529',
-    'lg;': '\u2276',
-    'lgE;': '\u2a91',
-    'lHar;': '\u2962',
-    'lhard;': '\u21bd',
-    'lharu;': '\u21bc',
-    'lharul;': '\u296a',
-    'lhblk;': '\u2584',
-    'LJcy;': '\u0409',
-    'ljcy;': '\u0459',
-    'Ll;': '\u22d8',
-    'll;': '\u226a',
-    'llarr;': '\u21c7',
-    'llcorner;': '\u231e',
-    'Lleftarrow;': '\u21da',
-    'llhard;': '\u296b',
-    'lltri;': '\u25fa',
-    'Lmidot;': '\u013f',
-    'lmidot;': '\u0140',
-    'lmoust;': '\u23b0',
-    'lmoustache;': '\u23b0',
-    'lnap;': '\u2a89',
-    'lnapprox;': '\u2a89',
-    'lnE;': '\u2268',
-    'lne;': '\u2a87',
-    'lneq;': '\u2a87',
-    'lneqq;': '\u2268',
-    'lnsim;': '\u22e6',
-    'loang;': '\u27ec',
-    'loarr;': '\u21fd',
-    'lobrk;': '\u27e6',
-    'LongLeftArrow;': '\u27f5',
-    'Longleftarrow;': '\u27f8',
-    'longleftarrow;': '\u27f5',
-    'LongLeftRightArrow;': '\u27f7',
-    'Longleftrightarrow;': '\u27fa',
-    'longleftrightarrow;': '\u27f7',
-    'longmapsto;': '\u27fc',
-    'LongRightArrow;': '\u27f6',
-    'Longrightarrow;': '\u27f9',
-    'longrightarrow;': '\u27f6',
-    'looparrowleft;': '\u21ab',
-    'looparrowright;': '\u21ac',
-    'lopar;': '\u2985',
-    'Lopf;': '\U0001d543',
-    'lopf;': '\U0001d55d',
-    'loplus;': '\u2a2d',
-    'lotimes;': '\u2a34',
-    'lowast;': '\u2217',
-    'lowbar;': '_',
-    'LowerLeftArrow;': '\u2199',
-    'LowerRightArrow;': '\u2198',
-    'loz;': '\u25ca',
-    'lozenge;': '\u25ca',
-    'lozf;': '\u29eb',
-    'lpar;': '(',
-    'lparlt;': '\u2993',
-    'lrarr;': '\u21c6',
-    'lrcorner;': '\u231f',
-    'lrhar;': '\u21cb',
-    'lrhard;': '\u296d',
-    'lrm;': '\u200e',
-    'lrtri;': '\u22bf',
-    'lsaquo;': '\u2039',
-    'Lscr;': '\u2112',
-    'lscr;': '\U0001d4c1',
-    'Lsh;': '\u21b0',
-    'lsh;': '\u21b0',
-    'lsim;': '\u2272',
-    'lsime;': '\u2a8d',
-    'lsimg;': '\u2a8f',
-    'lsqb;': '[',
-    'lsquo;': '\u2018',
-    'lsquor;': '\u201a',
-    'Lstrok;': '\u0141',
-    'lstrok;': '\u0142',
-    'LT': '<',
-    'lt': '<',
-    'LT;': '<',
-    'Lt;': '\u226a',
-    'lt;': '<',
-    'ltcc;': '\u2aa6',
-    'ltcir;': '\u2a79',
-    'ltdot;': '\u22d6',
-    'lthree;': '\u22cb',
-    'ltimes;': '\u22c9',
-    'ltlarr;': '\u2976',
-    'ltquest;': '\u2a7b',
-    'ltri;': '\u25c3',
-    'ltrie;': '\u22b4',
-    'ltrif;': '\u25c2',
-    'ltrPar;': '\u2996',
-    'lurdshar;': '\u294a',
-    'luruhar;': '\u2966',
-    'lvertneqq;': '\u2268\ufe00',
-    'lvnE;': '\u2268\ufe00',
-    'macr': '\xaf',
-    'macr;': '\xaf',
-    'male;': '\u2642',
-    'malt;': '\u2720',
-    'maltese;': '\u2720',
-    'Map;': '\u2905',
-    'map;': '\u21a6',
-    'mapsto;': '\u21a6',
-    'mapstodown;': '\u21a7',
-    'mapstoleft;': '\u21a4',
-    'mapstoup;': '\u21a5',
-    'marker;': '\u25ae',
-    'mcomma;': '\u2a29',
-    'Mcy;': '\u041c',
-    'mcy;': '\u043c',
-    'mdash;': '\u2014',
-    'mDDot;': '\u223a',
-    'measuredangle;': '\u2221',
-    'MediumSpace;': '\u205f',
-    'Mellintrf;': '\u2133',
-    'Mfr;': '\U0001d510',
-    'mfr;': '\U0001d52a',
-    'mho;': '\u2127',
-    'micro': '\xb5',
-    'micro;': '\xb5',
-    'mid;': '\u2223',
-    'midast;': '*',
-    'midcir;': '\u2af0',
-    'middot': '\xb7',
-    'middot;': '\xb7',
-    'minus;': '\u2212',
-    'minusb;': '\u229f',
-    'minusd;': '\u2238',
-    'minusdu;': '\u2a2a',
-    'MinusPlus;': '\u2213',
-    'mlcp;': '\u2adb',
-    'mldr;': '\u2026',
-    'mnplus;': '\u2213',
-    'models;': '\u22a7',
-    'Mopf;': '\U0001d544',
-    'mopf;': '\U0001d55e',
-    'mp;': '\u2213',
-    'Mscr;': '\u2133',
-    'mscr;': '\U0001d4c2',
-    'mstpos;': '\u223e',
-    'Mu;': '\u039c',
-    'mu;': '\u03bc',
-    'multimap;': '\u22b8',
-    'mumap;': '\u22b8',
-    'nabla;': '\u2207',
-    'Nacute;': '\u0143',
-    'nacute;': '\u0144',
-    'nang;': '\u2220\u20d2',
-    'nap;': '\u2249',
-    'napE;': '\u2a70\u0338',
-    'napid;': '\u224b\u0338',
-    'napos;': '\u0149',
-    'napprox;': '\u2249',
-    'natur;': '\u266e',
-    'natural;': '\u266e',
-    'naturals;': '\u2115',
-    'nbsp': '\xa0',
-    'nbsp;': '\xa0',
-    'nbump;': '\u224e\u0338',
-    'nbumpe;': '\u224f\u0338',
-    'ncap;': '\u2a43',
-    'Ncaron;': '\u0147',
-    'ncaron;': '\u0148',
-    'Ncedil;': '\u0145',
-    'ncedil;': '\u0146',
-    'ncong;': '\u2247',
-    'ncongdot;': '\u2a6d\u0338',
-    'ncup;': '\u2a42',
-    'Ncy;': '\u041d',
-    'ncy;': '\u043d',
-    'ndash;': '\u2013',
-    'ne;': '\u2260',
-    'nearhk;': '\u2924',
-    'neArr;': '\u21d7',
-    'nearr;': '\u2197',
-    'nearrow;': '\u2197',
-    'nedot;': '\u2250\u0338',
-    'NegativeMediumSpace;': '\u200b',
-    'NegativeThickSpace;': '\u200b',
-    'NegativeThinSpace;': '\u200b',
-    'NegativeVeryThinSpace;': '\u200b',
-    'nequiv;': '\u2262',
-    'nesear;': '\u2928',
-    'nesim;': '\u2242\u0338',
-    'NestedGreaterGreater;': '\u226b',
-    'NestedLessLess;': '\u226a',
-    'NewLine;': '\n',
-    'nexist;': '\u2204',
-    'nexists;': '\u2204',
-    'Nfr;': '\U0001d511',
-    'nfr;': '\U0001d52b',
-    'ngE;': '\u2267\u0338',
-    'nge;': '\u2271',
-    'ngeq;': '\u2271',
-    'ngeqq;': '\u2267\u0338',
-    'ngeqslant;': '\u2a7e\u0338',
-    'nges;': '\u2a7e\u0338',
-    'nGg;': '\u22d9\u0338',
-    'ngsim;': '\u2275',
-    'nGt;': '\u226b\u20d2',
-    'ngt;': '\u226f',
-    'ngtr;': '\u226f',
-    'nGtv;': '\u226b\u0338',
-    'nhArr;': '\u21ce',
-    'nharr;': '\u21ae',
-    'nhpar;': '\u2af2',
-    'ni;': '\u220b',
-    'nis;': '\u22fc',
-    'nisd;': '\u22fa',
-    'niv;': '\u220b',
-    'NJcy;': '\u040a',
-    'njcy;': '\u045a',
-    'nlArr;': '\u21cd',
-    'nlarr;': '\u219a',
-    'nldr;': '\u2025',
-    'nlE;': '\u2266\u0338',
-    'nle;': '\u2270',
-    'nLeftarrow;': '\u21cd',
-    'nleftarrow;': '\u219a',
-    'nLeftrightarrow;': '\u21ce',
-    'nleftrightarrow;': '\u21ae',
-    'nleq;': '\u2270',
-    'nleqq;': '\u2266\u0338',
-    'nleqslant;': '\u2a7d\u0338',
-    'nles;': '\u2a7d\u0338',
-    'nless;': '\u226e',
-    'nLl;': '\u22d8\u0338',
-    'nlsim;': '\u2274',
-    'nLt;': '\u226a\u20d2',
-    'nlt;': '\u226e',
-    'nltri;': '\u22ea',
-    'nltrie;': '\u22ec',
-    'nLtv;': '\u226a\u0338',
-    'nmid;': '\u2224',
-    'NoBreak;': '\u2060',
-    'NonBreakingSpace;': '\xa0',
-    'Nopf;': '\u2115',
-    'nopf;': '\U0001d55f',
-    'not': '\xac',
-    'Not;': '\u2aec',
-    'not;': '\xac',
-    'NotCongruent;': '\u2262',
-    'NotCupCap;': '\u226d',
-    'NotDoubleVerticalBar;': '\u2226',
-    'NotElement;': '\u2209',
-    'NotEqual;': '\u2260',
-    'NotEqualTilde;': '\u2242\u0338',
-    'NotExists;': '\u2204',
-    'NotGreater;': '\u226f',
-    'NotGreaterEqual;': '\u2271',
-    'NotGreaterFullEqual;': '\u2267\u0338',
-    'NotGreaterGreater;': '\u226b\u0338',
-    'NotGreaterLess;': '\u2279',
-    'NotGreaterSlantEqual;': '\u2a7e\u0338',
-    'NotGreaterTilde;': '\u2275',
-    'NotHumpDownHump;': '\u224e\u0338',
-    'NotHumpEqual;': '\u224f\u0338',
-    'notin;': '\u2209',
-    'notindot;': '\u22f5\u0338',
-    'notinE;': '\u22f9\u0338',
-    'notinva;': '\u2209',
-    'notinvb;': '\u22f7',
-    'notinvc;': '\u22f6',
-    'NotLeftTriangle;': '\u22ea',
-    'NotLeftTriangleBar;': '\u29cf\u0338',
-    'NotLeftTriangleEqual;': '\u22ec',
-    'NotLess;': '\u226e',
-    'NotLessEqual;': '\u2270',
-    'NotLessGreater;': '\u2278',
-    'NotLessLess;': '\u226a\u0338',
-    'NotLessSlantEqual;': '\u2a7d\u0338',
-    'NotLessTilde;': '\u2274',
-    'NotNestedGreaterGreater;': '\u2aa2\u0338',
-    'NotNestedLessLess;': '\u2aa1\u0338',
-    'notni;': '\u220c',
-    'notniva;': '\u220c',
-    'notnivb;': '\u22fe',
-    'notnivc;': '\u22fd',
-    'NotPrecedes;': '\u2280',
-    'NotPrecedesEqual;': '\u2aaf\u0338',
-    'NotPrecedesSlantEqual;': '\u22e0',
-    'NotReverseElement;': '\u220c',
-    'NotRightTriangle;': '\u22eb',
-    'NotRightTriangleBar;': '\u29d0\u0338',
-    'NotRightTriangleEqual;': '\u22ed',
-    'NotSquareSubset;': '\u228f\u0338',
-    'NotSquareSubsetEqual;': '\u22e2',
-    'NotSquareSuperset;': '\u2290\u0338',
-    'NotSquareSupersetEqual;': '\u22e3',
-    'NotSubset;': '\u2282\u20d2',
-    'NotSubsetEqual;': '\u2288',
-    'NotSucceeds;': '\u2281',
-    'NotSucceedsEqual;': '\u2ab0\u0338',
-    'NotSucceedsSlantEqual;': '\u22e1',
-    'NotSucceedsTilde;': '\u227f\u0338',
-    'NotSuperset;': '\u2283\u20d2',
-    'NotSupersetEqual;': '\u2289',
-    'NotTilde;': '\u2241',
-    'NotTildeEqual;': '\u2244',
-    'NotTildeFullEqual;': '\u2247',
-    'NotTildeTilde;': '\u2249',
-    'NotVerticalBar;': '\u2224',
-    'npar;': '\u2226',
-    'nparallel;': '\u2226',
-    'nparsl;': '\u2afd\u20e5',
-    'npart;': '\u2202\u0338',
-    'npolint;': '\u2a14',
-    'npr;': '\u2280',
-    'nprcue;': '\u22e0',
-    'npre;': '\u2aaf\u0338',
-    'nprec;': '\u2280',
-    'npreceq;': '\u2aaf\u0338',
-    'nrArr;': '\u21cf',
-    'nrarr;': '\u219b',
-    'nrarrc;': '\u2933\u0338',
-    'nrarrw;': '\u219d\u0338',
-    'nRightarrow;': '\u21cf',
-    'nrightarrow;': '\u219b',
-    'nrtri;': '\u22eb',
-    'nrtrie;': '\u22ed',
-    'nsc;': '\u2281',
-    'nsccue;': '\u22e1',
-    'nsce;': '\u2ab0\u0338',
-    'Nscr;': '\U0001d4a9',
-    'nscr;': '\U0001d4c3',
-    'nshortmid;': '\u2224',
-    'nshortparallel;': '\u2226',
-    'nsim;': '\u2241',
-    'nsime;': '\u2244',
-    'nsimeq;': '\u2244',
-    'nsmid;': '\u2224',
-    'nspar;': '\u2226',
-    'nsqsube;': '\u22e2',
-    'nsqsupe;': '\u22e3',
-    'nsub;': '\u2284',
-    'nsubE;': '\u2ac5\u0338',
-    'nsube;': '\u2288',
-    'nsubset;': '\u2282\u20d2',
-    'nsubseteq;': '\u2288',
-    'nsubseteqq;': '\u2ac5\u0338',
-    'nsucc;': '\u2281',
-    'nsucceq;': '\u2ab0\u0338',
-    'nsup;': '\u2285',
-    'nsupE;': '\u2ac6\u0338',
-    'nsupe;': '\u2289',
-    'nsupset;': '\u2283\u20d2',
-    'nsupseteq;': '\u2289',
-    'nsupseteqq;': '\u2ac6\u0338',
-    'ntgl;': '\u2279',
-    'Ntilde': '\xd1',
-    'ntilde': '\xf1',
-    'Ntilde;': '\xd1',
-    'ntilde;': '\xf1',
-    'ntlg;': '\u2278',
-    'ntriangleleft;': '\u22ea',
-    'ntrianglelefteq;': '\u22ec',
-    'ntriangleright;': '\u22eb',
-    'ntrianglerighteq;': '\u22ed',
-    'Nu;': '\u039d',
-    'nu;': '\u03bd',
-    'num;': '#',
-    'numero;': '\u2116',
-    'numsp;': '\u2007',
-    'nvap;': '\u224d\u20d2',
-    'nVDash;': '\u22af',
-    'nVdash;': '\u22ae',
-    'nvDash;': '\u22ad',
-    'nvdash;': '\u22ac',
-    'nvge;': '\u2265\u20d2',
-    'nvgt;': '>\u20d2',
-    'nvHarr;': '\u2904',
-    'nvinfin;': '\u29de',
-    'nvlArr;': '\u2902',
-    'nvle;': '\u2264\u20d2',
-    'nvlt;': '<\u20d2',
-    'nvltrie;': '\u22b4\u20d2',
-    'nvrArr;': '\u2903',
-    'nvrtrie;': '\u22b5\u20d2',
-    'nvsim;': '\u223c\u20d2',
-    'nwarhk;': '\u2923',
-    'nwArr;': '\u21d6',
-    'nwarr;': '\u2196',
-    'nwarrow;': '\u2196',
-    'nwnear;': '\u2927',
-    'Oacute': '\xd3',
-    'oacute': '\xf3',
-    'Oacute;': '\xd3',
-    'oacute;': '\xf3',
-    'oast;': '\u229b',
-    'ocir;': '\u229a',
-    'Ocirc': '\xd4',
-    'ocirc': '\xf4',
-    'Ocirc;': '\xd4',
-    'ocirc;': '\xf4',
-    'Ocy;': '\u041e',
-    'ocy;': '\u043e',
-    'odash;': '\u229d',
-    'Odblac;': '\u0150',
-    'odblac;': '\u0151',
-    'odiv;': '\u2a38',
-    'odot;': '\u2299',
-    'odsold;': '\u29bc',
-    'OElig;': '\u0152',
-    'oelig;': '\u0153',
-    'ofcir;': '\u29bf',
-    'Ofr;': '\U0001d512',
-    'ofr;': '\U0001d52c',
-    'ogon;': '\u02db',
-    'Ograve': '\xd2',
-    'ograve': '\xf2',
-    'Ograve;': '\xd2',
-    'ograve;': '\xf2',
-    'ogt;': '\u29c1',
-    'ohbar;': '\u29b5',
-    'ohm;': '\u03a9',
-    'oint;': '\u222e',
-    'olarr;': '\u21ba',
-    'olcir;': '\u29be',
-    'olcross;': '\u29bb',
-    'oline;': '\u203e',
-    'olt;': '\u29c0',
-    'Omacr;': '\u014c',
-    'omacr;': '\u014d',
-    'Omega;': '\u03a9',
-    'omega;': '\u03c9',
-    'Omicron;': '\u039f',
-    'omicron;': '\u03bf',
-    'omid;': '\u29b6',
-    'ominus;': '\u2296',
-    'Oopf;': '\U0001d546',
-    'oopf;': '\U0001d560',
-    'opar;': '\u29b7',
-    'OpenCurlyDoubleQuote;': '\u201c',
-    'OpenCurlyQuote;': '\u2018',
-    'operp;': '\u29b9',
-    'oplus;': '\u2295',
-    'Or;': '\u2a54',
-    'or;': '\u2228',
-    'orarr;': '\u21bb',
-    'ord;': '\u2a5d',
-    'order;': '\u2134',
-    'orderof;': '\u2134',
-    'ordf': '\xaa',
-    'ordf;': '\xaa',
-    'ordm': '\xba',
-    'ordm;': '\xba',
-    'origof;': '\u22b6',
-    'oror;': '\u2a56',
-    'orslope;': '\u2a57',
-    'orv;': '\u2a5b',
-    'oS;': '\u24c8',
-    'Oscr;': '\U0001d4aa',
-    'oscr;': '\u2134',
-    'Oslash': '\xd8',
-    'oslash': '\xf8',
-    'Oslash;': '\xd8',
-    'oslash;': '\xf8',
-    'osol;': '\u2298',
-    'Otilde': '\xd5',
-    'otilde': '\xf5',
-    'Otilde;': '\xd5',
-    'otilde;': '\xf5',
-    'Otimes;': '\u2a37',
-    'otimes;': '\u2297',
-    'otimesas;': '\u2a36',
-    'Ouml': '\xd6',
-    'ouml': '\xf6',
-    'Ouml;': '\xd6',
-    'ouml;': '\xf6',
-    'ovbar;': '\u233d',
-    'OverBar;': '\u203e',
-    'OverBrace;': '\u23de',
-    'OverBracket;': '\u23b4',
-    'OverParenthesis;': '\u23dc',
-    'par;': '\u2225',
-    'para': '\xb6',
-    'para;': '\xb6',
-    'parallel;': '\u2225',
-    'parsim;': '\u2af3',
-    'parsl;': '\u2afd',
-    'part;': '\u2202',
-    'PartialD;': '\u2202',
-    'Pcy;': '\u041f',
-    'pcy;': '\u043f',
-    'percnt;': '%',
-    'period;': '.',
-    'permil;': '\u2030',
-    'perp;': '\u22a5',
-    'pertenk;': '\u2031',
-    'Pfr;': '\U0001d513',
-    'pfr;': '\U0001d52d',
-    'Phi;': '\u03a6',
-    'phi;': '\u03c6',
-    'phiv;': '\u03d5',
-    'phmmat;': '\u2133',
-    'phone;': '\u260e',
-    'Pi;': '\u03a0',
-    'pi;': '\u03c0',
-    'pitchfork;': '\u22d4',
-    'piv;': '\u03d6',
-    'planck;': '\u210f',
-    'planckh;': '\u210e',
-    'plankv;': '\u210f',
-    'plus;': '+',
-    'plusacir;': '\u2a23',
-    'plusb;': '\u229e',
-    'pluscir;': '\u2a22',
-    'plusdo;': '\u2214',
-    'plusdu;': '\u2a25',
-    'pluse;': '\u2a72',
-    'PlusMinus;': '\xb1',
-    'plusmn': '\xb1',
-    'plusmn;': '\xb1',
-    'plussim;': '\u2a26',
-    'plustwo;': '\u2a27',
-    'pm;': '\xb1',
-    'Poincareplane;': '\u210c',
-    'pointint;': '\u2a15',
-    'Popf;': '\u2119',
-    'popf;': '\U0001d561',
-    'pound': '\xa3',
-    'pound;': '\xa3',
-    'Pr;': '\u2abb',
-    'pr;': '\u227a',
-    'prap;': '\u2ab7',
-    'prcue;': '\u227c',
-    'prE;': '\u2ab3',
-    'pre;': '\u2aaf',
-    'prec;': '\u227a',
-    'precapprox;': '\u2ab7',
-    'preccurlyeq;': '\u227c',
-    'Precedes;': '\u227a',
-    'PrecedesEqual;': '\u2aaf',
-    'PrecedesSlantEqual;': '\u227c',
-    'PrecedesTilde;': '\u227e',
-    'preceq;': '\u2aaf',
-    'precnapprox;': '\u2ab9',
-    'precneqq;': '\u2ab5',
-    'precnsim;': '\u22e8',
-    'precsim;': '\u227e',
-    'Prime;': '\u2033',
-    'prime;': '\u2032',
-    'primes;': '\u2119',
-    'prnap;': '\u2ab9',
-    'prnE;': '\u2ab5',
-    'prnsim;': '\u22e8',
-    'prod;': '\u220f',
-    'Product;': '\u220f',
-    'profalar;': '\u232e',
-    'profline;': '\u2312',
-    'profsurf;': '\u2313',
-    'prop;': '\u221d',
-    'Proportion;': '\u2237',
-    'Proportional;': '\u221d',
-    'propto;': '\u221d',
-    'prsim;': '\u227e',
-    'prurel;': '\u22b0',
-    'Pscr;': '\U0001d4ab',
-    'pscr;': '\U0001d4c5',
-    'Psi;': '\u03a8',
-    'psi;': '\u03c8',
-    'puncsp;': '\u2008',
-    'Qfr;': '\U0001d514',
-    'qfr;': '\U0001d52e',
-    'qint;': '\u2a0c',
-    'Qopf;': '\u211a',
-    'qopf;': '\U0001d562',
-    'qprime;': '\u2057',
-    'Qscr;': '\U0001d4ac',
-    'qscr;': '\U0001d4c6',
-    'quaternions;': '\u210d',
-    'quatint;': '\u2a16',
-    'quest;': '?',
-    'questeq;': '\u225f',
-    'QUOT': '"',
-    'quot': '"',
-    'QUOT;': '"',
-    'quot;': '"',
-    'rAarr;': '\u21db',
-    'race;': '\u223d\u0331',
-    'Racute;': '\u0154',
-    'racute;': '\u0155',
-    'radic;': '\u221a',
-    'raemptyv;': '\u29b3',
-    'Rang;': '\u27eb',
-    'rang;': '\u27e9',
-    'rangd;': '\u2992',
-    'range;': '\u29a5',
-    'rangle;': '\u27e9',
-    'raquo': '\xbb',
-    'raquo;': '\xbb',
-    'Rarr;': '\u21a0',
-    'rArr;': '\u21d2',
-    'rarr;': '\u2192',
-    'rarrap;': '\u2975',
-    'rarrb;': '\u21e5',
-    'rarrbfs;': '\u2920',
-    'rarrc;': '\u2933',
-    'rarrfs;': '\u291e',
-    'rarrhk;': '\u21aa',
-    'rarrlp;': '\u21ac',
-    'rarrpl;': '\u2945',
-    'rarrsim;': '\u2974',
-    'Rarrtl;': '\u2916',
-    'rarrtl;': '\u21a3',
-    'rarrw;': '\u219d',
-    'rAtail;': '\u291c',
-    'ratail;': '\u291a',
-    'ratio;': '\u2236',
-    'rationals;': '\u211a',
-    'RBarr;': '\u2910',
-    'rBarr;': '\u290f',
-    'rbarr;': '\u290d',
-    'rbbrk;': '\u2773',
-    'rbrace;': '}',
-    'rbrack;': ']',
-    'rbrke;': '\u298c',
-    'rbrksld;': '\u298e',
-    'rbrkslu;': '\u2990',
-    'Rcaron;': '\u0158',
-    'rcaron;': '\u0159',
-    'Rcedil;': '\u0156',
-    'rcedil;': '\u0157',
-    'rceil;': '\u2309',
-    'rcub;': '}',
-    'Rcy;': '\u0420',
-    'rcy;': '\u0440',
-    'rdca;': '\u2937',
-    'rdldhar;': '\u2969',
-    'rdquo;': '\u201d',
-    'rdquor;': '\u201d',
-    'rdsh;': '\u21b3',
-    'Re;': '\u211c',
-    'real;': '\u211c',
-    'realine;': '\u211b',
-    'realpart;': '\u211c',
-    'reals;': '\u211d',
-    'rect;': '\u25ad',
-    'REG': '\xae',
-    'reg': '\xae',
-    'REG;': '\xae',
-    'reg;': '\xae',
-    'ReverseElement;': '\u220b',
-    'ReverseEquilibrium;': '\u21cb',
-    'ReverseUpEquilibrium;': '\u296f',
-    'rfisht;': '\u297d',
-    'rfloor;': '\u230b',
-    'Rfr;': '\u211c',
-    'rfr;': '\U0001d52f',
-    'rHar;': '\u2964',
-    'rhard;': '\u21c1',
-    'rharu;': '\u21c0',
-    'rharul;': '\u296c',
-    'Rho;': '\u03a1',
-    'rho;': '\u03c1',
-    'rhov;': '\u03f1',
-    'RightAngleBracket;': '\u27e9',
-    'RightArrow;': '\u2192',
-    'Rightarrow;': '\u21d2',
-    'rightarrow;': '\u2192',
-    'RightArrowBar;': '\u21e5',
-    'RightArrowLeftArrow;': '\u21c4',
-    'rightarrowtail;': '\u21a3',
-    'RightCeiling;': '\u2309',
-    'RightDoubleBracket;': '\u27e7',
-    'RightDownTeeVector;': '\u295d',
-    'RightDownVector;': '\u21c2',
-    'RightDownVectorBar;': '\u2955',
-    'RightFloor;': '\u230b',
-    'rightharpoondown;': '\u21c1',
-    'rightharpoonup;': '\u21c0',
-    'rightleftarrows;': '\u21c4',
-    'rightleftharpoons;': '\u21cc',
-    'rightrightarrows;': '\u21c9',
-    'rightsquigarrow;': '\u219d',
-    'RightTee;': '\u22a2',
-    'RightTeeArrow;': '\u21a6',
-    'RightTeeVector;': '\u295b',
-    'rightthreetimes;': '\u22cc',
-    'RightTriangle;': '\u22b3',
-    'RightTriangleBar;': '\u29d0',
-    'RightTriangleEqual;': '\u22b5',
-    'RightUpDownVector;': '\u294f',
-    'RightUpTeeVector;': '\u295c',
-    'RightUpVector;': '\u21be',
-    'RightUpVectorBar;': '\u2954',
-    'RightVector;': '\u21c0',
-    'RightVectorBar;': '\u2953',
-    'ring;': '\u02da',
-    'risingdotseq;': '\u2253',
-    'rlarr;': '\u21c4',
-    'rlhar;': '\u21cc',
-    'rlm;': '\u200f',
-    'rmoust;': '\u23b1',
-    'rmoustache;': '\u23b1',
-    'rnmid;': '\u2aee',
-    'roang;': '\u27ed',
-    'roarr;': '\u21fe',
-    'robrk;': '\u27e7',
-    'ropar;': '\u2986',
-    'Ropf;': '\u211d',
-    'ropf;': '\U0001d563',
-    'roplus;': '\u2a2e',
-    'rotimes;': '\u2a35',
-    'RoundImplies;': '\u2970',
-    'rpar;': ')',
-    'rpargt;': '\u2994',
-    'rppolint;': '\u2a12',
-    'rrarr;': '\u21c9',
-    'Rrightarrow;': '\u21db',
-    'rsaquo;': '\u203a',
-    'Rscr;': '\u211b',
-    'rscr;': '\U0001d4c7',
-    'Rsh;': '\u21b1',
-    'rsh;': '\u21b1',
-    'rsqb;': ']',
-    'rsquo;': '\u2019',
-    'rsquor;': '\u2019',
-    'rthree;': '\u22cc',
-    'rtimes;': '\u22ca',
-    'rtri;': '\u25b9',
-    'rtrie;': '\u22b5',
-    'rtrif;': '\u25b8',
-    'rtriltri;': '\u29ce',
-    'RuleDelayed;': '\u29f4',
-    'ruluhar;': '\u2968',
-    'rx;': '\u211e',
-    'Sacute;': '\u015a',
-    'sacute;': '\u015b',
-    'sbquo;': '\u201a',
-    'Sc;': '\u2abc',
-    'sc;': '\u227b',
-    'scap;': '\u2ab8',
-    'Scaron;': '\u0160',
-    'scaron;': '\u0161',
-    'sccue;': '\u227d',
-    'scE;': '\u2ab4',
-    'sce;': '\u2ab0',
-    'Scedil;': '\u015e',
-    'scedil;': '\u015f',
-    'Scirc;': '\u015c',
-    'scirc;': '\u015d',
-    'scnap;': '\u2aba',
-    'scnE;': '\u2ab6',
-    'scnsim;': '\u22e9',
-    'scpolint;': '\u2a13',
-    'scsim;': '\u227f',
-    'Scy;': '\u0421',
-    'scy;': '\u0441',
-    'sdot;': '\u22c5',
-    'sdotb;': '\u22a1',
-    'sdote;': '\u2a66',
-    'searhk;': '\u2925',
-    'seArr;': '\u21d8',
-    'searr;': '\u2198',
-    'searrow;': '\u2198',
-    'sect': '\xa7',
-    'sect;': '\xa7',
-    'semi;': ';',
-    'seswar;': '\u2929',
-    'setminus;': '\u2216',
-    'setmn;': '\u2216',
-    'sext;': '\u2736',
-    'Sfr;': '\U0001d516',
-    'sfr;': '\U0001d530',
-    'sfrown;': '\u2322',
-    'sharp;': '\u266f',
-    'SHCHcy;': '\u0429',
-    'shchcy;': '\u0449',
-    'SHcy;': '\u0428',
-    'shcy;': '\u0448',
-    'ShortDownArrow;': '\u2193',
-    'ShortLeftArrow;': '\u2190',
-    'shortmid;': '\u2223',
-    'shortparallel;': '\u2225',
-    'ShortRightArrow;': '\u2192',
-    'ShortUpArrow;': '\u2191',
-    'shy': '\xad',
-    'shy;': '\xad',
-    'Sigma;': '\u03a3',
-    'sigma;': '\u03c3',
-    'sigmaf;': '\u03c2',
-    'sigmav;': '\u03c2',
-    'sim;': '\u223c',
-    'simdot;': '\u2a6a',
-    'sime;': '\u2243',
-    'simeq;': '\u2243',
-    'simg;': '\u2a9e',
-    'simgE;': '\u2aa0',
-    'siml;': '\u2a9d',
-    'simlE;': '\u2a9f',
-    'simne;': '\u2246',
-    'simplus;': '\u2a24',
-    'simrarr;': '\u2972',
-    'slarr;': '\u2190',
-    'SmallCircle;': '\u2218',
-    'smallsetminus;': '\u2216',
-    'smashp;': '\u2a33',
-    'smeparsl;': '\u29e4',
-    'smid;': '\u2223',
-    'smile;': '\u2323',
-    'smt;': '\u2aaa',
-    'smte;': '\u2aac',
-    'smtes;': '\u2aac\ufe00',
-    'SOFTcy;': '\u042c',
-    'softcy;': '\u044c',
-    'sol;': '/',
-    'solb;': '\u29c4',
-    'solbar;': '\u233f',
-    'Sopf;': '\U0001d54a',
-    'sopf;': '\U0001d564',
-    'spades;': '\u2660',
-    'spadesuit;': '\u2660',
-    'spar;': '\u2225',
-    'sqcap;': '\u2293',
-    'sqcaps;': '\u2293\ufe00',
-    'sqcup;': '\u2294',
-    'sqcups;': '\u2294\ufe00',
-    'Sqrt;': '\u221a',
-    'sqsub;': '\u228f',
-    'sqsube;': '\u2291',
-    'sqsubset;': '\u228f',
-    'sqsubseteq;': '\u2291',
-    'sqsup;': '\u2290',
-    'sqsupe;': '\u2292',
-    'sqsupset;': '\u2290',
-    'sqsupseteq;': '\u2292',
-    'squ;': '\u25a1',
-    'Square;': '\u25a1',
-    'square;': '\u25a1',
-    'SquareIntersection;': '\u2293',
-    'SquareSubset;': '\u228f',
-    'SquareSubsetEqual;': '\u2291',
-    'SquareSuperset;': '\u2290',
-    'SquareSupersetEqual;': '\u2292',
-    'SquareUnion;': '\u2294',
-    'squarf;': '\u25aa',
-    'squf;': '\u25aa',
-    'srarr;': '\u2192',
-    'Sscr;': '\U0001d4ae',
-    'sscr;': '\U0001d4c8',
-    'ssetmn;': '\u2216',
-    'ssmile;': '\u2323',
-    'sstarf;': '\u22c6',
-    'Star;': '\u22c6',
-    'star;': '\u2606',
-    'starf;': '\u2605',
-    'straightepsilon;': '\u03f5',
-    'straightphi;': '\u03d5',
-    'strns;': '\xaf',
-    'Sub;': '\u22d0',
-    'sub;': '\u2282',
-    'subdot;': '\u2abd',
-    'subE;': '\u2ac5',
-    'sube;': '\u2286',
-    'subedot;': '\u2ac3',
-    'submult;': '\u2ac1',
-    'subnE;': '\u2acb',
-    'subne;': '\u228a',
-    'subplus;': '\u2abf',
-    'subrarr;': '\u2979',
-    'Subset;': '\u22d0',
-    'subset;': '\u2282',
-    'subseteq;': '\u2286',
-    'subseteqq;': '\u2ac5',
-    'SubsetEqual;': '\u2286',
-    'subsetneq;': '\u228a',
-    'subsetneqq;': '\u2acb',
-    'subsim;': '\u2ac7',
-    'subsub;': '\u2ad5',
-    'subsup;': '\u2ad3',
-    'succ;': '\u227b',
-    'succapprox;': '\u2ab8',
-    'succcurlyeq;': '\u227d',
-    'Succeeds;': '\u227b',
-    'SucceedsEqual;': '\u2ab0',
-    'SucceedsSlantEqual;': '\u227d',
-    'SucceedsTilde;': '\u227f',
-    'succeq;': '\u2ab0',
-    'succnapprox;': '\u2aba',
-    'succneqq;': '\u2ab6',
-    'succnsim;': '\u22e9',
-    'succsim;': '\u227f',
-    'SuchThat;': '\u220b',
-    'Sum;': '\u2211',
-    'sum;': '\u2211',
-    'sung;': '\u266a',
-    'sup1': '\xb9',
-    'sup1;': '\xb9',
-    'sup2': '\xb2',
-    'sup2;': '\xb2',
-    'sup3': '\xb3',
-    'sup3;': '\xb3',
-    'Sup;': '\u22d1',
-    'sup;': '\u2283',
-    'supdot;': '\u2abe',
-    'supdsub;': '\u2ad8',
-    'supE;': '\u2ac6',
-    'supe;': '\u2287',
-    'supedot;': '\u2ac4',
-    'Superset;': '\u2283',
-    'SupersetEqual;': '\u2287',
-    'suphsol;': '\u27c9',
-    'suphsub;': '\u2ad7',
-    'suplarr;': '\u297b',
-    'supmult;': '\u2ac2',
-    'supnE;': '\u2acc',
-    'supne;': '\u228b',
-    'supplus;': '\u2ac0',
-    'Supset;': '\u22d1',
-    'supset;': '\u2283',
-    'supseteq;': '\u2287',
-    'supseteqq;': '\u2ac6',
-    'supsetneq;': '\u228b',
-    'supsetneqq;': '\u2acc',
-    'supsim;': '\u2ac8',
-    'supsub;': '\u2ad4',
-    'supsup;': '\u2ad6',
-    'swarhk;': '\u2926',
-    'swArr;': '\u21d9',
-    'swarr;': '\u2199',
-    'swarrow;': '\u2199',
-    'swnwar;': '\u292a',
-    'szlig': '\xdf',
-    'szlig;': '\xdf',
-    'Tab;': '\t',
-    'target;': '\u2316',
-    'Tau;': '\u03a4',
-    'tau;': '\u03c4',
-    'tbrk;': '\u23b4',
-    'Tcaron;': '\u0164',
-    'tcaron;': '\u0165',
-    'Tcedil;': '\u0162',
-    'tcedil;': '\u0163',
-    'Tcy;': '\u0422',
-    'tcy;': '\u0442',
-    'tdot;': '\u20db',
-    'telrec;': '\u2315',
-    'Tfr;': '\U0001d517',
-    'tfr;': '\U0001d531',
-    'there4;': '\u2234',
-    'Therefore;': '\u2234',
-    'therefore;': '\u2234',
-    'Theta;': '\u0398',
-    'theta;': '\u03b8',
-    'thetasym;': '\u03d1',
-    'thetav;': '\u03d1',
-    'thickapprox;': '\u2248',
-    'thicksim;': '\u223c',
-    'ThickSpace;': '\u205f\u200a',
-    'thinsp;': '\u2009',
-    'ThinSpace;': '\u2009',
-    'thkap;': '\u2248',
-    'thksim;': '\u223c',
-    'THORN': '\xde',
-    'thorn': '\xfe',
-    'THORN;': '\xde',
-    'thorn;': '\xfe',
-    'Tilde;': '\u223c',
-    'tilde;': '\u02dc',
-    'TildeEqual;': '\u2243',
-    'TildeFullEqual;': '\u2245',
-    'TildeTilde;': '\u2248',
-    'times': '\xd7',
-    'times;': '\xd7',
-    'timesb;': '\u22a0',
-    'timesbar;': '\u2a31',
-    'timesd;': '\u2a30',
-    'tint;': '\u222d',
-    'toea;': '\u2928',
-    'top;': '\u22a4',
-    'topbot;': '\u2336',
-    'topcir;': '\u2af1',
-    'Topf;': '\U0001d54b',
-    'topf;': '\U0001d565',
-    'topfork;': '\u2ada',
-    'tosa;': '\u2929',
-    'tprime;': '\u2034',
-    'TRADE;': '\u2122',
-    'trade;': '\u2122',
-    'triangle;': '\u25b5',
-    'triangledown;': '\u25bf',
-    'triangleleft;': '\u25c3',
-    'trianglelefteq;': '\u22b4',
-    'triangleq;': '\u225c',
-    'triangleright;': '\u25b9',
-    'trianglerighteq;': '\u22b5',
-    'tridot;': '\u25ec',
-    'trie;': '\u225c',
-    'triminus;': '\u2a3a',
-    'TripleDot;': '\u20db',
-    'triplus;': '\u2a39',
-    'trisb;': '\u29cd',
-    'tritime;': '\u2a3b',
-    'trpezium;': '\u23e2',
-    'Tscr;': '\U0001d4af',
-    'tscr;': '\U0001d4c9',
-    'TScy;': '\u0426',
-    'tscy;': '\u0446',
-    'TSHcy;': '\u040b',
-    'tshcy;': '\u045b',
-    'Tstrok;': '\u0166',
-    'tstrok;': '\u0167',
-    'twixt;': '\u226c',
-    'twoheadleftarrow;': '\u219e',
-    'twoheadrightarrow;': '\u21a0',
-    'Uacute': '\xda',
-    'uacute': '\xfa',
-    'Uacute;': '\xda',
-    'uacute;': '\xfa',
-    'Uarr;': '\u219f',
-    'uArr;': '\u21d1',
-    'uarr;': '\u2191',
-    'Uarrocir;': '\u2949',
-    'Ubrcy;': '\u040e',
-    'ubrcy;': '\u045e',
-    'Ubreve;': '\u016c',
-    'ubreve;': '\u016d',
-    'Ucirc': '\xdb',
-    'ucirc': '\xfb',
-    'Ucirc;': '\xdb',
-    'ucirc;': '\xfb',
-    'Ucy;': '\u0423',
-    'ucy;': '\u0443',
-    'udarr;': '\u21c5',
-    'Udblac;': '\u0170',
-    'udblac;': '\u0171',
-    'udhar;': '\u296e',
-    'ufisht;': '\u297e',
-    'Ufr;': '\U0001d518',
-    'ufr;': '\U0001d532',
-    'Ugrave': '\xd9',
-    'ugrave': '\xf9',
-    'Ugrave;': '\xd9',
-    'ugrave;': '\xf9',
-    'uHar;': '\u2963',
-    'uharl;': '\u21bf',
-    'uharr;': '\u21be',
-    'uhblk;': '\u2580',
-    'ulcorn;': '\u231c',
-    'ulcorner;': '\u231c',
-    'ulcrop;': '\u230f',
-    'ultri;': '\u25f8',
-    'Umacr;': '\u016a',
-    'umacr;': '\u016b',
-    'uml': '\xa8',
-    'uml;': '\xa8',
-    'UnderBar;': '_',
-    'UnderBrace;': '\u23df',
-    'UnderBracket;': '\u23b5',
-    'UnderParenthesis;': '\u23dd',
-    'Union;': '\u22c3',
-    'UnionPlus;': '\u228e',
-    'Uogon;': '\u0172',
-    'uogon;': '\u0173',
-    'Uopf;': '\U0001d54c',
-    'uopf;': '\U0001d566',
-    'UpArrow;': '\u2191',
-    'Uparrow;': '\u21d1',
-    'uparrow;': '\u2191',
-    'UpArrowBar;': '\u2912',
-    'UpArrowDownArrow;': '\u21c5',
-    'UpDownArrow;': '\u2195',
-    'Updownarrow;': '\u21d5',
-    'updownarrow;': '\u2195',
-    'UpEquilibrium;': '\u296e',
-    'upharpoonleft;': '\u21bf',
-    'upharpoonright;': '\u21be',
-    'uplus;': '\u228e',
-    'UpperLeftArrow;': '\u2196',
-    'UpperRightArrow;': '\u2197',
-    'Upsi;': '\u03d2',
-    'upsi;': '\u03c5',
-    'upsih;': '\u03d2',
-    'Upsilon;': '\u03a5',
-    'upsilon;': '\u03c5',
-    'UpTee;': '\u22a5',
-    'UpTeeArrow;': '\u21a5',
-    'upuparrows;': '\u21c8',
-    'urcorn;': '\u231d',
-    'urcorner;': '\u231d',
-    'urcrop;': '\u230e',
-    'Uring;': '\u016e',
-    'uring;': '\u016f',
-    'urtri;': '\u25f9',
-    'Uscr;': '\U0001d4b0',
-    'uscr;': '\U0001d4ca',
-    'utdot;': '\u22f0',
-    'Utilde;': '\u0168',
-    'utilde;': '\u0169',
-    'utri;': '\u25b5',
-    'utrif;': '\u25b4',
-    'uuarr;': '\u21c8',
-    'Uuml': '\xdc',
-    'uuml': '\xfc',
-    'Uuml;': '\xdc',
-    'uuml;': '\xfc',
-    'uwangle;': '\u29a7',
-    'vangrt;': '\u299c',
-    'varepsilon;': '\u03f5',
-    'varkappa;': '\u03f0',
-    'varnothing;': '\u2205',
-    'varphi;': '\u03d5',
-    'varpi;': '\u03d6',
-    'varpropto;': '\u221d',
-    'vArr;': '\u21d5',
-    'varr;': '\u2195',
-    'varrho;': '\u03f1',
-    'varsigma;': '\u03c2',
-    'varsubsetneq;': '\u228a\ufe00',
-    'varsubsetneqq;': '\u2acb\ufe00',
-    'varsupsetneq;': '\u228b\ufe00',
-    'varsupsetneqq;': '\u2acc\ufe00',
-    'vartheta;': '\u03d1',
-    'vartriangleleft;': '\u22b2',
-    'vartriangleright;': '\u22b3',
-    'Vbar;': '\u2aeb',
-    'vBar;': '\u2ae8',
-    'vBarv;': '\u2ae9',
-    'Vcy;': '\u0412',
-    'vcy;': '\u0432',
-    'VDash;': '\u22ab',
-    'Vdash;': '\u22a9',
-    'vDash;': '\u22a8',
-    'vdash;': '\u22a2',
-    'Vdashl;': '\u2ae6',
-    'Vee;': '\u22c1',
-    'vee;': '\u2228',
-    'veebar;': '\u22bb',
-    'veeeq;': '\u225a',
-    'vellip;': '\u22ee',
-    'Verbar;': '\u2016',
-    'verbar;': '|',
-    'Vert;': '\u2016',
-    'vert;': '|',
-    'VerticalBar;': '\u2223',
-    'VerticalLine;': '|',
-    'VerticalSeparator;': '\u2758',
-    'VerticalTilde;': '\u2240',
-    'VeryThinSpace;': '\u200a',
-    'Vfr;': '\U0001d519',
-    'vfr;': '\U0001d533',
-    'vltri;': '\u22b2',
-    'vnsub;': '\u2282\u20d2',
-    'vnsup;': '\u2283\u20d2',
-    'Vopf;': '\U0001d54d',
-    'vopf;': '\U0001d567',
-    'vprop;': '\u221d',
-    'vrtri;': '\u22b3',
-    'Vscr;': '\U0001d4b1',
-    'vscr;': '\U0001d4cb',
-    'vsubnE;': '\u2acb\ufe00',
-    'vsubne;': '\u228a\ufe00',
-    'vsupnE;': '\u2acc\ufe00',
-    'vsupne;': '\u228b\ufe00',
-    'Vvdash;': '\u22aa',
-    'vzigzag;': '\u299a',
-    'Wcirc;': '\u0174',
-    'wcirc;': '\u0175',
-    'wedbar;': '\u2a5f',
-    'Wedge;': '\u22c0',
-    'wedge;': '\u2227',
-    'wedgeq;': '\u2259',
-    'weierp;': '\u2118',
-    'Wfr;': '\U0001d51a',
-    'wfr;': '\U0001d534',
-    'Wopf;': '\U0001d54e',
-    'wopf;': '\U0001d568',
-    'wp;': '\u2118',
-    'wr;': '\u2240',
-    'wreath;': '\u2240',
-    'Wscr;': '\U0001d4b2',
-    'wscr;': '\U0001d4cc',
-    'xcap;': '\u22c2',
-    'xcirc;': '\u25ef',
-    'xcup;': '\u22c3',
-    'xdtri;': '\u25bd',
-    'Xfr;': '\U0001d51b',
-    'xfr;': '\U0001d535',
-    'xhArr;': '\u27fa',
-    'xharr;': '\u27f7',
-    'Xi;': '\u039e',
-    'xi;': '\u03be',
-    'xlArr;': '\u27f8',
-    'xlarr;': '\u27f5',
-    'xmap;': '\u27fc',
-    'xnis;': '\u22fb',
-    'xodot;': '\u2a00',
-    'Xopf;': '\U0001d54f',
-    'xopf;': '\U0001d569',
-    'xoplus;': '\u2a01',
-    'xotime;': '\u2a02',
-    'xrArr;': '\u27f9',
-    'xrarr;': '\u27f6',
-    'Xscr;': '\U0001d4b3',
-    'xscr;': '\U0001d4cd',
-    'xsqcup;': '\u2a06',
-    'xuplus;': '\u2a04',
-    'xutri;': '\u25b3',
-    'xvee;': '\u22c1',
-    'xwedge;': '\u22c0',
-    'Yacute': '\xdd',
-    'yacute': '\xfd',
-    'Yacute;': '\xdd',
-    'yacute;': '\xfd',
-    'YAcy;': '\u042f',
-    'yacy;': '\u044f',
-    'Ycirc;': '\u0176',
-    'ycirc;': '\u0177',
-    'Ycy;': '\u042b',
-    'ycy;': '\u044b',
-    'yen': '\xa5',
-    'yen;': '\xa5',
-    'Yfr;': '\U0001d51c',
-    'yfr;': '\U0001d536',
-    'YIcy;': '\u0407',
-    'yicy;': '\u0457',
-    'Yopf;': '\U0001d550',
-    'yopf;': '\U0001d56a',
-    'Yscr;': '\U0001d4b4',
-    'yscr;': '\U0001d4ce',
-    'YUcy;': '\u042e',
-    'yucy;': '\u044e',
-    'yuml': '\xff',
-    'Yuml;': '\u0178',
-    'yuml;': '\xff',
-    'Zacute;': '\u0179',
-    'zacute;': '\u017a',
-    'Zcaron;': '\u017d',
-    'zcaron;': '\u017e',
-    'Zcy;': '\u0417',
-    'zcy;': '\u0437',
-    'Zdot;': '\u017b',
-    'zdot;': '\u017c',
-    'zeetrf;': '\u2128',
-    'ZeroWidthSpace;': '\u200b',
-    'Zeta;': '\u0396',
-    'zeta;': '\u03b6',
-    'Zfr;': '\u2128',
-    'zfr;': '\U0001d537',
-    'ZHcy;': '\u0416',
-    'zhcy;': '\u0436',
-    'zigrarr;': '\u21dd',
-    'Zopf;': '\u2124',
-    'zopf;': '\U0001d56b',
-    'Zscr;': '\U0001d4b5',
-    'zscr;': '\U0001d4cf',
-    'zwj;': '\u200d',
-    'zwnj;': '\u200c',
-}
-
-# maps the Unicode codepoint to the HTML entity name
-codepoint2name = {}
-
-# maps the HTML entity name to the character
-# (or a character reference if the character is outside the Latin-1 range)
-entitydefs = {}
-
-for (name, codepoint) in name2codepoint.items():
-    codepoint2name[codepoint] = name
-    entitydefs[name] = chr(codepoint)
-
-del name, codepoint
diff --git a/html.entities/metadata.txt b/html.entities/metadata.txt
deleted file mode 100644
index fcdd06f76..000000000
--- a/html.entities/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=cpython
-type=package
-version=3.3.3
diff --git a/html.entities/setup.py b/html.entities/setup.py
deleted file mode 100644
index 35172c9b2..000000000
--- a/html.entities/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-html.entities',
-      version='3.3.3',
-      description='CPython html.entities module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['html'])
diff --git a/html.parser/html/parser.py b/html.parser/html/parser.py
deleted file mode 100644
index c31320e2b..000000000
--- a/html.parser/html/parser.py
+++ /dev/null
@@ -1,532 +0,0 @@
-"""A parser for HTML and XHTML."""
-
-# This file is based on sgmllib.py, but the API is slightly different.
-
-# XXX There should be a way to distinguish between PCDATA (parsed
-# character data -- the normal case), RCDATA (replaceable character
-# data -- only char and entity references and end tags are special)
-# and CDATA (character data -- only end tags are special).
-
-
-import _markupbase
-import re
-import warnings
-
-# Regular expressions used for parsing
-
-interesting_normal = re.compile('[&<]')
-incomplete = re.compile('&[a-zA-Z#]')
-
-entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]')
-charref = re.compile('&#(?:[0-9]+|[xX][0-9a-fA-F]+)[^0-9a-fA-F]')
-
-starttagopen = re.compile('<[a-zA-Z]')
-piclose = re.compile('>')
-commentclose = re.compile(r'--\s*>')
-tagfind = re.compile('([a-zA-Z][-.a-zA-Z0-9:_]*)(?:\s|/(?!>))*')
-# see http://www.w3.org/TR/html5/tokenization.html#tag-open-state
-# and http://www.w3.org/TR/html5/tokenization.html#tag-name-state
-tagfind_tolerant = re.compile('[a-zA-Z][^\t\n\r\f />\\x00]*')
-# Note:
-#  1) the strict attrfind isn't really strict, but we can't make it
-#     correctly strict without breaking backward compatibility;
-#  2) if you change attrfind remember to update locatestarttagend too;
-#  3) if you change attrfind and/or locatestarttagend the parser will
-#     explode, so don't do it.
-attrfind = re.compile(
-    r'\s*([a-zA-Z_][-.:a-zA-Z_0-9]*)(\s*=\s*'
-    r'(\'[^\']*\'|"[^"]*"|[^\s"\'=<>`]*))?')
-attrfind_tolerant = re.compile(
-    r'((?<=[\'"\s/])[^\s/>][^\s/=>]*)(\s*=+\s*'
-    r'(\'[^\']*\'|"[^"]*"|(?![\'"])[^>\s]*))?(?:\s|/(?!>))*')
-locatestarttagend = re.compile(r"""
-  <[a-zA-Z][-.a-zA-Z0-9:_]*          # tag name
-  (?:\s+                             # whitespace before attribute name
-    (?:[a-zA-Z_][-.:a-zA-Z0-9_]*     # attribute name
-      (?:\s*=\s*                     # value indicator
-        (?:'[^']*'                   # LITA-enclosed value
-          |\"[^\"]*\"                # LIT-enclosed value
-          |[^'\">\s]+                # bare value
-         )
-       )?
-     )
-   )*
-  \s*                                # trailing whitespace
-""", re.VERBOSE)
-locatestarttagend_tolerant = re.compile(r"""
-  <[a-zA-Z][-.a-zA-Z0-9:_]*          # tag name
-  (?:[\s/]*                          # optional whitespace before attribute name
-    (?:(?<=['"\s/])[^\s/>][^\s/=>]*  # attribute name
-      (?:\s*=+\s*                    # value indicator
-        (?:'[^']*'                   # LITA-enclosed value
-          |"[^"]*"                   # LIT-enclosed value
-          |(?!['"])[^>\s]*           # bare value
-         )
-         (?:\s*,)*                   # possibly followed by a comma
-       )?(?:\s|/(?!>))*
-     )*
-   )?
-  \s*                                # trailing whitespace
-""", re.VERBOSE)
-endendtag = re.compile('>')
-# the HTML 5 spec, section 8.1.2.2, doesn't allow spaces between
-# </ and the tag name, so maybe this should be fixed
-endtagfind = re.compile('</\s*([a-zA-Z][-.a-zA-Z0-9:_]*)\s*>')
-
-
-class HTMLParseError(Exception):
-    """Exception raised for all parse errors."""
-
-    def __init__(self, msg, position=(None, None)):
-        assert msg
-        self.msg = msg
-        self.lineno = position[0]
-        self.offset = position[1]
-
-    def __str__(self):
-        result = self.msg
-        if self.lineno is not None:
-            result = result + ", at line %d" % self.lineno
-        if self.offset is not None:
-            result = result + ", column %d" % (self.offset + 1)
-        return result
-
-
-class HTMLParser(_markupbase.ParserBase):
-    """Find tags and other markup and call handler functions.
-
-    Usage:
-        p = HTMLParser()
-        p.feed(data)
-        ...
-        p.close()
-
-    Start tags are handled by calling self.handle_starttag() or
-    self.handle_startendtag(); end tags by self.handle_endtag().  The
-    data between tags is passed from the parser to the derived class
-    by calling self.handle_data() with the data as argument (the data
-    may be split up in arbitrary chunks).  Entity references are
-    passed by calling self.handle_entityref() with the entity
-    reference as the argument.  Numeric character references are
-    passed to self.handle_charref() with the string containing the
-    reference as the argument.
-    """
-
-    CDATA_CONTENT_ELEMENTS = ("script", "style")
-
-    def __init__(self, strict=False):
-        """Initialize and reset this instance.
-
-        If strict is set to False (the default) the parser will parse invalid
-        markup, otherwise it will raise an error.  Note that the strict mode
-        is deprecated.
-        """
-        if strict:
-            warnings.warn("The strict mode is deprecated.",
-                          DeprecationWarning, stacklevel=2)
-        self.strict = strict
-        self.reset()
-
-    def reset(self):
-        """Reset this instance.  Loses all unprocessed data."""
-        self.rawdata = ''
-        self.lasttag = '???'
-        self.interesting = interesting_normal
-        self.cdata_elem = None
-        _markupbase.ParserBase.reset(self)
-
-    def feed(self, data):
-        r"""Feed data to the parser.
-
-        Call this as often as you want, with as little or as much text
-        as you want (may include '\n').
-        """
-        self.rawdata = self.rawdata + data
-        self.goahead(0)
-
-    def close(self):
-        """Handle any buffered data."""
-        self.goahead(1)
-
-    def error(self, message):
-        raise HTMLParseError(message, self.getpos())
-
-    __starttag_text = None
-
-    def get_starttag_text(self):
-        """Return full source of start tag: '<...>'."""
-        return self.__starttag_text
-
-    def set_cdata_mode(self, elem):
-        self.cdata_elem = elem.lower()
-        self.interesting = re.compile(r'</\s*%s\s*>' % self.cdata_elem, re.I)
-
-    def clear_cdata_mode(self):
-        self.interesting = interesting_normal
-        self.cdata_elem = None
-
-    # Internal -- handle data as far as reasonable.  May leave state
-    # and data to be processed by a subsequent call.  If 'end' is
-    # true, force handling all data as if followed by EOF marker.
-    def goahead(self, end):
-        rawdata = self.rawdata
-        i = 0
-        n = len(rawdata)
-        while i < n:
-            match = self.interesting.search(rawdata, i) # < or &
-            if match:
-                j = match.start()
-            else:
-                if self.cdata_elem:
-                    break
-                j = n
-            if i < j: self.handle_data(rawdata[i:j])
-            i = self.updatepos(i, j)
-            if i == n: break
-            startswith = rawdata.startswith
-            if startswith('<', i):
-                if starttagopen.match(rawdata, i): # < + letter
-                    k = self.parse_starttag(i)
-                elif startswith("</", i):
-                    k = self.parse_endtag(i)
-                elif startswith("<!--", i):
-                    k = self.parse_comment(i)
-                elif startswith("<?", i):
-                    k = self.parse_pi(i)
-                elif startswith("<!", i):
-                    if self.strict:
-                        k = self.parse_declaration(i)
-                    else:
-                        k = self.parse_html_declaration(i)
-                elif (i + 1) < n:
-                    self.handle_data("<")
-                    k = i + 1
-                else:
-                    break
-                if k < 0:
-                    if not end:
-                        break
-                    if self.strict:
-                        self.error("EOF in middle of construct")
-                    k = rawdata.find('>', i + 1)
-                    if k < 0:
-                        k = rawdata.find('<', i + 1)
-                        if k < 0:
-                            k = i + 1
-                    else:
-                        k += 1
-                    self.handle_data(rawdata[i:k])
-                i = self.updatepos(i, k)
-            elif startswith("&#", i):
-                match = charref.match(rawdata, i)
-                if match:
-                    name = match.group()[2:-1]
-                    self.handle_charref(name)
-                    k = match.end()
-                    if not startswith(';', k-1):
-                        k = k - 1
-                    i = self.updatepos(i, k)
-                    continue
-                else:
-                    if ";" in rawdata[i:]: #bail by consuming &#
-                        self.handle_data(rawdata[0:2])
-                        i = self.updatepos(i, 2)
-                    break
-            elif startswith('&', i):
-                match = entityref.match(rawdata, i)
-                if match:
-                    name = match.group(1)
-                    self.handle_entityref(name)
-                    k = match.end()
-                    if not startswith(';', k-1):
-                        k = k - 1
-                    i = self.updatepos(i, k)
-                    continue
-                match = incomplete.match(rawdata, i)
-                if match:
-                    # match.group() will contain at least 2 chars
-                    if end and match.group() == rawdata[i:]:
-                        if self.strict:
-                            self.error("EOF in middle of entity or char ref")
-                        else:
-                            k = match.end()
-                            if k <= i:
-                                k = n
-                            i = self.updatepos(i, i + 1)
-                    # incomplete
-                    break
-                elif (i + 1) < n:
-                    # not the end of the buffer, and can't be confused
-                    # with some other construct
-                    self.handle_data("&")
-                    i = self.updatepos(i, i + 1)
-                else:
-                    break
-            else:
-                assert 0, "interesting.search() lied"
-        # end while
-        if end and i < n and not self.cdata_elem:
-            self.handle_data(rawdata[i:n])
-            i = self.updatepos(i, n)
-        self.rawdata = rawdata[i:]
-
-    # Internal -- parse html declarations, return length or -1 if not terminated
-    # See w3.org/TR/html5/tokenization.html#markup-declaration-open-state
-    # See also parse_declaration in _markupbase
-    def parse_html_declaration(self, i):
-        rawdata = self.rawdata
-        assert rawdata[i:i+2] == '<!', ('unexpected call to '
-                                        'parse_html_declaration()')
-        if rawdata[i:i+4] == '<!--':
-            # this case is actually already handled in goahead()
-            return self.parse_comment(i)
-        elif rawdata[i:i+3] == '<![':
-            return self.parse_marked_section(i)
-        elif rawdata[i:i+9].lower() == '<!doctype':
-            # find the closing >
-            gtpos = rawdata.find('>', i+9)
-            if gtpos == -1:
-                return -1
-            self.handle_decl(rawdata[i+2:gtpos])
-            return gtpos+1
-        else:
-            return self.parse_bogus_comment(i)
-
-    # Internal -- parse bogus comment, return length or -1 if not terminated
-    # see http://www.w3.org/TR/html5/tokenization.html#bogus-comment-state
-    def parse_bogus_comment(self, i, report=1):
-        rawdata = self.rawdata
-        assert rawdata[i:i+2] in ('<!', '</'), ('unexpected call to '
-                                                'parse_comment()')
-        pos = rawdata.find('>', i+2)
-        if pos == -1:
-            return -1
-        if report:
-            self.handle_comment(rawdata[i+2:pos])
-        return pos + 1
-
-    # Internal -- parse processing instr, return end or -1 if not terminated
-    def parse_pi(self, i):
-        rawdata = self.rawdata
-        assert rawdata[i:i+2] == '<?', 'unexpected call to parse_pi()'
-        match = piclose.search(rawdata, i+2) # >
-        if not match:
-            return -1
-        j = match.start()
-        self.handle_pi(rawdata[i+2: j])
-        j = match.end()
-        return j
-
-    # Internal -- handle starttag, return end or -1 if not terminated
-    def parse_starttag(self, i):
-        self.__starttag_text = None
-        endpos = self.check_for_whole_start_tag(i)
-        if endpos < 0:
-            return endpos
-        rawdata = self.rawdata
-        self.__starttag_text = rawdata[i:endpos]
-
-        # Now parse the data between i+1 and j into a tag and attrs
-        attrs = []
-        match = tagfind.match(rawdata, i+1)
-        assert match, 'unexpected call to parse_starttag()'
-        k = match.end()
-        self.lasttag = tag = match.group(1).lower()
-        while k < endpos:
-            if self.strict:
-                m = attrfind.match(rawdata, k)
-            else:
-                m = attrfind_tolerant.match(rawdata, k)
-            if not m:
-                break
-            attrname, rest, attrvalue = m.group(1, 2, 3)
-            if not rest:
-                attrvalue = None
-            elif attrvalue[:1] == '\'' == attrvalue[-1:] or \
-                 attrvalue[:1] == '"' == attrvalue[-1:]:
-                attrvalue = attrvalue[1:-1]
-            if attrvalue:
-                attrvalue = self.unescape(attrvalue)
-            attrs.append((attrname.lower(), attrvalue))
-            k = m.end()
-
-        end = rawdata[k:endpos].strip()
-        if end not in (">", "/>"):
-            lineno, offset = self.getpos()
-            if "\n" in self.__starttag_text:
-                lineno = lineno + self.__starttag_text.count("\n")
-                offset = len(self.__starttag_text) \
-                         - self.__starttag_text.rfind("\n")
-            else:
-                offset = offset + len(self.__starttag_text)
-            if self.strict:
-                self.error("junk characters in start tag: %r"
-                           % (rawdata[k:endpos][:20],))
-            self.handle_data(rawdata[i:endpos])
-            return endpos
-        if end.endswith('/>'):
-            # XHTML-style empty tag: <span attr="value" />
-            self.handle_startendtag(tag, attrs)
-        else:
-            self.handle_starttag(tag, attrs)
-            if tag in self.CDATA_CONTENT_ELEMENTS:
-                self.set_cdata_mode(tag)
-        return endpos
-
-    # Internal -- check to see if we have a complete starttag; return end
-    # or -1 if incomplete.
-    def check_for_whole_start_tag(self, i):
-        rawdata = self.rawdata
-        if self.strict:
-            m = locatestarttagend.match(rawdata, i)
-        else:
-            m = locatestarttagend_tolerant.match(rawdata, i)
-        if m:
-            j = m.end()
-            next = rawdata[j:j+1]
-            if next == ">":
-                return j + 1
-            if next == "/":
-                if rawdata.startswith("/>", j):
-                    return j + 2
-                if rawdata.startswith("/", j):
-                    # buffer boundary
-                    return -1
-                # else bogus input
-                if self.strict:
-                    self.updatepos(i, j + 1)
-                    self.error("malformed empty start tag")
-                if j > i:
-                    return j
-                else:
-                    return i + 1
-            if next == "":
-                # end of input
-                return -1
-            if next in ("abcdefghijklmnopqrstuvwxyz=/"
-                        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
-                # end of input in or before attribute value, or we have the
-                # '/' from a '/>' ending
-                return -1
-            if self.strict:
-                self.updatepos(i, j)
-                self.error("malformed start tag")
-            if j > i:
-                return j
-            else:
-                return i + 1
-        raise AssertionError("we should not get here!")
-
-    # Internal -- parse endtag, return end or -1 if incomplete
-    def parse_endtag(self, i):
-        rawdata = self.rawdata
-        assert rawdata[i:i+2] == "</", "unexpected call to parse_endtag"
-        match = endendtag.search(rawdata, i+1) # >
-        if not match:
-            return -1
-        gtpos = match.end()
-        match = endtagfind.match(rawdata, i) # </ + tag + >
-        if not match:
-            if self.cdata_elem is not None:
-                self.handle_data(rawdata[i:gtpos])
-                return gtpos
-            if self.strict:
-                self.error("bad end tag: %r" % (rawdata[i:gtpos],))
-            # find the name: w3.org/TR/html5/tokenization.html#tag-name-state
-            namematch = tagfind_tolerant.match(rawdata, i+2)
-            if not namematch:
-                # w3.org/TR/html5/tokenization.html#end-tag-open-state
-                if rawdata[i:i+3] == '</>':
-                    return i+3
-                else:
-                    return self.parse_bogus_comment(i)
-            tagname = namematch.group().lower()
-            # consume and ignore other stuff between the name and the >
-            # Note: this is not 100% correct, since we might have things like
-            # </tag attr=">">, but looking for > after tha name should cover
-            # most of the cases and is much simpler
-            gtpos = rawdata.find('>', namematch.end())
-            self.handle_endtag(tagname)
-            return gtpos+1
-
-        elem = match.group(1).lower() # script or style
-        if self.cdata_elem is not None:
-            if elem != self.cdata_elem:
-                self.handle_data(rawdata[i:gtpos])
-                return gtpos
-
-        self.handle_endtag(elem.lower())
-        self.clear_cdata_mode()
-        return gtpos
-
-    # Overridable -- finish processing of start+end tag: <tag.../>
-    def handle_startendtag(self, tag, attrs):
-        self.handle_starttag(tag, attrs)
-        self.handle_endtag(tag)
-
-    # Overridable -- handle start tag
-    def handle_starttag(self, tag, attrs):
-        pass
-
-    # Overridable -- handle end tag
-    def handle_endtag(self, tag):
-        pass
-
-    # Overridable -- handle character reference
-    def handle_charref(self, name):
-        pass
-
-    # Overridable -- handle entity reference
-    def handle_entityref(self, name):
-        pass
-
-    # Overridable -- handle data
-    def handle_data(self, data):
-        pass
-
-    # Overridable -- handle comment
-    def handle_comment(self, data):
-        pass
-
-    # Overridable -- handle declaration
-    def handle_decl(self, decl):
-        pass
-
-    # Overridable -- handle processing instruction
-    def handle_pi(self, data):
-        pass
-
-    def unknown_decl(self, data):
-        if self.strict:
-            self.error("unknown declaration: %r" % (data,))
-
-    # Internal -- helper to remove special character quoting
-    def unescape(self, s):
-        if '&' not in s:
-            return s
-        def replaceEntities(s):
-            s = s.groups()[0]
-            try:
-                if s[0] == "#":
-                    s = s[1:]
-                    if s[0] in ['x','X']:
-                        c = int(s[1:].rstrip(';'), 16)
-                    else:
-                        c = int(s.rstrip(';'))
-                    return chr(c)
-            except ValueError:
-                return '&#' + s
-            else:
-                from html.entities import html5
-                if s in html5:
-                    return html5[s]
-                elif s.endswith(';'):
-                    return '&' + s
-                for x in range(2, len(s)):
-                    if s[:x] in html5:
-                        return html5[s[:x]] + s[x:]
-                else:
-                    return '&' + s
-
-        return re.sub(r"&(#?[xX]?(?:[0-9a-fA-F]+;|\w{1,32};?))",
-                      replaceEntities, s, flags=re.ASCII)
diff --git a/html.parser/metadata.txt b/html.parser/metadata.txt
deleted file mode 100644
index 59b9ad84a..000000000
--- a/html.parser/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 3.3.3-1
-depends = _markupbase, warnings, html.entities, re-pcre
diff --git a/html.parser/setup.py b/html.parser/setup.py
deleted file mode 100644
index eca51b0df..000000000
--- a/html.parser/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-html.parser',
-      version='3.3.3-1',
-      description='CPython html.parser module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['html'],
-      install_requires=['micropython-_markupbase', 'micropython-warnings', 'micropython-html.entities', 'micropython-re-pcre'])
diff --git a/html/html/__init__.py b/html/html/__init__.py
deleted file mode 100644
index 0422cb519..000000000
--- a/html/html/__init__.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""
-General functions for HTML manipulation.
-"""
-
-
-_escape_map = {ord('&'): '&amp;', ord('<'): '&lt;', ord('>'): '&gt;'}
-_escape_map_full = {ord('&'): '&amp;', ord('<'): '&lt;', ord('>'): '&gt;',
-                    ord('"'): '&quot;', ord('\''): '&#x27;'}
-
-# NB: this is a candidate for a bytes/string polymorphic interface
-
-def escape(s, quote=True):
-    """
-    Replace special characters "&", "<" and ">" to HTML-safe sequences.
-    If the optional flag quote is true (the default), the quotation mark
-    characters, both double quote (") and single quote (') characters are also
-    translated.
-    """
-    import string
-    if quote:
-        return string.translate(s, _escape_map_full)
-    return string.translate(s, _escape_map)
diff --git a/html/metadata.txt b/html/metadata.txt
deleted file mode 100644
index 4a538dcaf..000000000
--- a/html/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 3.3.3-1
-depends = string
diff --git a/html/setup.py b/html/setup.py
deleted file mode 100644
index d4cf7f922..000000000
--- a/html/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-html',
-      version='3.3.3-1',
-      description='CPython html module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['html'],
-      install_requires=['micropython-string'])
diff --git a/http.client/http/client.py b/http.client/http/client.py
deleted file mode 100644
index 98f883c10..000000000
--- a/http.client/http/client.py
+++ /dev/null
@@ -1,1279 +0,0 @@
-"""HTTP/1.1 client library
-
-<intro stuff goes here>
-<other stuff, too>
-
-HTTPConnection goes through a number of "states", which define when a client
-may legally make another request or fetch the response for a particular
-request. This diagram details these state transitions:
-
-    (null)
-      |
-      | HTTPConnection()
-      v
-    Idle
-      |
-      | putrequest()
-      v
-    Request-started
-      |
-      | ( putheader() )*  endheaders()
-      v
-    Request-sent
-      |
-      | response = getresponse()
-      v
-    Unread-response   [Response-headers-read]
-      |\____________________
-      |                     |
-      | response.read()     | putrequest()
-      v                     v
-    Idle                  Req-started-unread-response
-                     ______/|
-                   /        |
-   response.read() |        | ( putheader() )*  endheaders()
-                   v        v
-       Request-started    Req-sent-unread-response
-                            |
-                            | response.read()
-                            v
-                          Request-sent
-
-This diagram presents the following rules:
-  -- a second request may not be started until {response-headers-read}
-  -- a response [object] cannot be retrieved until {request-sent}
-  -- there is no differentiation between an unread response body and a
-     partially read response body
-
-Note: this enforcement is applied by the HTTPConnection class. The
-      HTTPResponse class does not enforce this state machine, which
-      implies sophisticated clients may accelerate the request/response
-      pipeline. Caution should be taken, though: accelerating the states
-      beyond the above pattern may imply knowledge of the server's
-      connection-close behavior for certain requests. For example, it
-      is impossible to tell whether the server will close the connection
-      UNTIL the response headers have been read; this means that further
-      requests cannot be placed into the pipeline until it is known that
-      the server will NOT be closing the connection.
-
-Logical State                  __state            __response
--------------                  -------            ----------
-Idle                           _CS_IDLE           None
-Request-started                _CS_REQ_STARTED    None
-Request-sent                   _CS_REQ_SENT       None
-Unread-response                _CS_IDLE           <response_class>
-Req-started-unread-response    _CS_REQ_STARTED    <response_class>
-Req-sent-unread-response       _CS_REQ_SENT       <response_class>
-"""
-
-import email.parser
-import email.message
-import io
-import os
-import socket
-import collections
-from urllib.parse import urlsplit
-import warnings
-
-__all__ = ["HTTPResponse", "HTTPConnection",
-           "HTTPException", "NotConnected", "UnknownProtocol",
-           "UnknownTransferEncoding", "UnimplementedFileMode",
-           "IncompleteRead", "InvalidURL", "ImproperConnectionState",
-           "CannotSendRequest", "CannotSendHeader", "ResponseNotReady",
-           "BadStatusLine", "error", "responses"]
-
-HTTP_PORT = 80
-HTTPS_PORT = 443
-
-_UNKNOWN = 'UNKNOWN'
-
-# connection states
-_CS_IDLE = 'Idle'
-_CS_REQ_STARTED = 'Request-started'
-_CS_REQ_SENT = 'Request-sent'
-
-# status codes
-# informational
-CONTINUE = 100
-SWITCHING_PROTOCOLS = 101
-PROCESSING = 102
-
-# successful
-OK = 200
-CREATED = 201
-ACCEPTED = 202
-NON_AUTHORITATIVE_INFORMATION = 203
-NO_CONTENT = 204
-RESET_CONTENT = 205
-PARTIAL_CONTENT = 206
-MULTI_STATUS = 207
-IM_USED = 226
-
-# redirection
-MULTIPLE_CHOICES = 300
-MOVED_PERMANENTLY = 301
-FOUND = 302
-SEE_OTHER = 303
-NOT_MODIFIED = 304
-USE_PROXY = 305
-TEMPORARY_REDIRECT = 307
-
-# client error
-BAD_REQUEST = 400
-UNAUTHORIZED = 401
-PAYMENT_REQUIRED = 402
-FORBIDDEN = 403
-NOT_FOUND = 404
-METHOD_NOT_ALLOWED = 405
-NOT_ACCEPTABLE = 406
-PROXY_AUTHENTICATION_REQUIRED = 407
-REQUEST_TIMEOUT = 408
-CONFLICT = 409
-GONE = 410
-LENGTH_REQUIRED = 411
-PRECONDITION_FAILED = 412
-REQUEST_ENTITY_TOO_LARGE = 413
-REQUEST_URI_TOO_LONG = 414
-UNSUPPORTED_MEDIA_TYPE = 415
-REQUESTED_RANGE_NOT_SATISFIABLE = 416
-EXPECTATION_FAILED = 417
-UNPROCESSABLE_ENTITY = 422
-LOCKED = 423
-FAILED_DEPENDENCY = 424
-UPGRADE_REQUIRED = 426
-PRECONDITION_REQUIRED = 428
-TOO_MANY_REQUESTS = 429
-REQUEST_HEADER_FIELDS_TOO_LARGE = 431
-
-# server error
-INTERNAL_SERVER_ERROR = 500
-NOT_IMPLEMENTED = 501
-BAD_GATEWAY = 502
-SERVICE_UNAVAILABLE = 503
-GATEWAY_TIMEOUT = 504
-HTTP_VERSION_NOT_SUPPORTED = 505
-INSUFFICIENT_STORAGE = 507
-NOT_EXTENDED = 510
-NETWORK_AUTHENTICATION_REQUIRED = 511
-
-# Mapping status codes to official W3C names
-responses = {
-    100: 'Continue',
-    101: 'Switching Protocols',
-
-    200: 'OK',
-    201: 'Created',
-    202: 'Accepted',
-    203: 'Non-Authoritative Information',
-    204: 'No Content',
-    205: 'Reset Content',
-    206: 'Partial Content',
-
-    300: 'Multiple Choices',
-    301: 'Moved Permanently',
-    302: 'Found',
-    303: 'See Other',
-    304: 'Not Modified',
-    305: 'Use Proxy',
-    306: '(Unused)',
-    307: 'Temporary Redirect',
-
-    400: 'Bad Request',
-    401: 'Unauthorized',
-    402: 'Payment Required',
-    403: 'Forbidden',
-    404: 'Not Found',
-    405: 'Method Not Allowed',
-    406: 'Not Acceptable',
-    407: 'Proxy Authentication Required',
-    408: 'Request Timeout',
-    409: 'Conflict',
-    410: 'Gone',
-    411: 'Length Required',
-    412: 'Precondition Failed',
-    413: 'Request Entity Too Large',
-    414: 'Request-URI Too Long',
-    415: 'Unsupported Media Type',
-    416: 'Requested Range Not Satisfiable',
-    417: 'Expectation Failed',
-    428: 'Precondition Required',
-    429: 'Too Many Requests',
-    431: 'Request Header Fields Too Large',
-
-    500: 'Internal Server Error',
-    501: 'Not Implemented',
-    502: 'Bad Gateway',
-    503: 'Service Unavailable',
-    504: 'Gateway Timeout',
-    505: 'HTTP Version Not Supported',
-    511: 'Network Authentication Required',
-}
-
-# maximal amount of data to read at one time in _safe_read
-MAXAMOUNT = 1048576
-
-# maximal line length when calling readline().
-_MAXLINE = 65536
-_MAXHEADERS = 100
-
-
-class HTTPMessage(email.message.Message):
-    # XXX The only usage of this method is in
-    # http.server.CGIHTTPRequestHandler.  Maybe move the code there so
-    # that it doesn't need to be part of the public API.  The API has
-    # never been defined so this could cause backwards compatibility
-    # issues.
-
-    def getallmatchingheaders(self, name):
-        """Find all header lines matching a given header name.
-
-        Look through the list of headers and find all lines matching a given
-        header name (and their continuation lines).  A list of the lines is
-        returned, without interpretation.  If the header does not occur, an
-        empty list is returned.  If the header occurs multiple times, all
-        occurrences are returned.  Case is not important in the header name.
-
-        """
-        name = name.lower() + ':'
-        n = len(name)
-        lst = []
-        hit = 0
-        for line in self.keys():
-            if line[:n].lower() == name:
-                hit = 1
-            elif not line[:1].isspace():
-                hit = 0
-            if hit:
-                lst.append(line)
-        return lst
-
-def parse_headers(fp, _class=HTTPMessage):
-    """Parses only RFC2822 headers from a file pointer.
-
-    email Parser wants to see strings rather than bytes.
-    But a TextIOWrapper around self.rfile would buffer too many bytes
-    from the stream, bytes which we later need to read as bytes.
-    So we read the correct bytes here, as bytes, for email Parser
-    to parse.
-
-    """
-    headers = []
-    while True:
-        line = fp.readline(_MAXLINE + 1)
-        if len(line) > _MAXLINE:
-            raise LineTooLong("header line")
-        headers.append(line)
-        if len(headers) > _MAXHEADERS:
-            raise HTTPException("got more than %d headers" % _MAXHEADERS)
-        if line in (b'\r\n', b'\n', b''):
-            break
-    hstring = b''.join(headers).decode('iso-8859-1')
-    return email.parser.Parser(_class=_class).parsestr(hstring)
-
-
-_strict_sentinel = object()
-
-class HTTPResponse:
-#class HTTPResponse(io.RawIOBase):
-
-    # See RFC 2616 sec 19.6 and RFC 1945 sec 6 for details.
-
-    # The bytes from the socket object are iso-8859-1 strings.
-    # See RFC 2616 sec 2.2 which notes an exception for MIME-encoded
-    # text following RFC 2047.  The basic status line parsing only
-    # accepts iso-8859-1.
-
-    def __init__(self, sock, debuglevel=0, strict=_strict_sentinel, method=None, url=None):
-        # If the response includes a content-length header, we need to
-        # make sure that the client doesn't read more than the
-        # specified number of bytes.  If it does, it will block until
-        # the server times out and closes the connection.  This will
-        # happen if a self.fp.read() is done (without a size) whether
-        # self.fp is buffered or not.  So, no self.fp.read() by
-        # clients unless they know what they are doing.
-        self.fp = sock.makefile("rb")
-        self.debuglevel = debuglevel
-        if strict is not _strict_sentinel:
-            warnings.warn("the 'strict' argument isn't supported anymore; "
-                "http.client now always assumes HTTP/1.x compliant servers.",
-                DeprecationWarning, 2)
-        self._method = method
-
-        # The HTTPResponse object is returned via urllib.  The clients
-        # of http and urllib expect different attributes for the
-        # headers.  headers is used here and supports urllib.  msg is
-        # provided as a backwards compatibility layer for http
-        # clients.
-
-        self.headers = self.msg = None
-
-        # from the Status-Line of the response
-        self.version = _UNKNOWN # HTTP-Version
-        self.status = _UNKNOWN  # Status-Code
-        self.reason = _UNKNOWN  # Reason-Phrase
-
-        self.chunked = _UNKNOWN         # is "chunked" being used?
-        self.chunk_left = _UNKNOWN      # bytes left to read in current chunk
-        self.length = _UNKNOWN          # number of bytes left in response
-        self.will_close = _UNKNOWN      # conn will close at end of response
-
-    def _read_status(self):
-        line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
-        if len(line) > _MAXLINE:
-            raise LineTooLong("status line")
-        if self.debuglevel > 0:
-            print("reply:", repr(line))
-        if not line:
-            # Presumably, the server closed the connection before
-            # sending a valid response.
-            raise BadStatusLine(line)
-        try:
-            version, status, reason = line.split(None, 2)
-        except ValueError:
-            try:
-                version, status = line.split(None, 1)
-                reason = ""
-            except ValueError:
-                # empty version will cause next test to fail.
-                version = ""
-        if not version.startswith("HTTP/"):
-            self._close_conn()
-            raise BadStatusLine(line)
-
-        # The status code is a three-digit number
-        try:
-            status = int(status)
-            if status < 100 or status > 999:
-                raise BadStatusLine(line)
-        except ValueError:
-            raise BadStatusLine(line)
-        return version, status, reason
-
-    def begin(self):
-        if self.headers is not None:
-            # we've already started reading the response
-            return
-
-        # read until we get a non-100 response
-        while True:
-            version, status, reason = self._read_status()
-            if status != CONTINUE:
-                break
-            # skip the header from the 100 response
-            while True:
-                skip = self.fp.readline(_MAXLINE + 1)
-                if len(skip) > _MAXLINE:
-                    raise LineTooLong("header line")
-                skip = skip.strip()
-                if not skip:
-                    break
-                if self.debuglevel > 0:
-                    print("header:", skip)
-
-        self.code = self.status = status
-        self.reason = reason.strip()
-        if version in ("HTTP/1.0", "HTTP/0.9"):
-            # Some servers might still return "0.9", treat it as 1.0 anyway
-            self.version = 10
-        elif version.startswith("HTTP/1."):
-            self.version = 11   # use HTTP/1.1 code for HTTP/1.x where x>=1
-        else:
-            raise UnknownProtocol(version)
-
-        self.headers = self.msg = parse_headers(self.fp)
-
-        if self.debuglevel > 0:
-            for hdr in self.headers:
-                print("header:", hdr, end=" ")
-
-        # are we using the chunked-style of transfer encoding?
-        tr_enc = self.headers.get("transfer-encoding")
-        if tr_enc and tr_enc.lower() == "chunked":
-            self.chunked = True
-            self.chunk_left = None
-        else:
-            self.chunked = False
-
-        # will the connection close at the end of the response?
-        self.will_close = self._check_close()
-
-        # do we have a Content-Length?
-        # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked"
-        self.length = None
-        length = self.headers.get("content-length")
-
-         # are we using the chunked-style of transfer encoding?
-        tr_enc = self.headers.get("transfer-encoding")
-        if length and not self.chunked:
-            try:
-                self.length = int(length)
-            except ValueError:
-                self.length = None
-            else:
-                if self.length < 0:  # ignore nonsensical negative lengths
-                    self.length = None
-        else:
-            self.length = None
-
-        # does the body have a fixed length? (of zero)
-        if (status == NO_CONTENT or status == NOT_MODIFIED or
-            100 <= status < 200 or      # 1xx codes
-            self._method == "HEAD"):
-            self.length = 0
-
-        # if the connection remains open, and we aren't using chunked, and
-        # a content-length was not provided, then assume that the connection
-        # WILL close.
-        if (not self.will_close and
-            not self.chunked and
-            self.length is None):
-            self.will_close = True
-
-    def _check_close(self):
-        conn = self.headers.get("connection")
-        if self.version == 11:
-            # An HTTP/1.1 proxy is assumed to stay open unless
-            # explicitly closed.
-            conn = self.headers.get("connection")
-            if conn and "close" in conn.lower():
-                return True
-            return False
-
-        # Some HTTP/1.0 implementations have support for persistent
-        # connections, using rules different than HTTP/1.1.
-
-        # For older HTTP, Keep-Alive indicates persistent connection.
-        if self.headers.get("keep-alive"):
-            return False
-
-        # At least Akamai returns a "Connection: Keep-Alive" header,
-        # which was supposed to be sent by the client.
-        if conn and "keep-alive" in conn.lower():
-            return False
-
-        # Proxy-Connection is a netscape hack.
-        pconn = self.headers.get("proxy-connection")
-        if pconn and "keep-alive" in pconn.lower():
-            return False
-
-        # otherwise, assume it will close
-        return True
-
-    def _close_conn(self):
-        fp = self.fp
-        self.fp = None
-        fp.close()
-
-    def close(self):
-        super().close() # set "closed" flag
-        if self.fp:
-            self._close_conn()
-
-    # These implementations are for the benefit of io.BufferedReader.
-
-    # XXX This class should probably be revised to act more like
-    # the "raw stream" that BufferedReader expects.
-
-    def flush(self):
-        super().flush()
-        if self.fp:
-            self.fp.flush()
-
-    def readable(self):
-        return True
-
-    # End of "raw stream" methods
-
-    def isclosed(self):
-        """True if the connection is closed."""
-        # NOTE: it is possible that we will not ever call self.close(). This
-        #       case occurs when will_close is TRUE, length is None, and we
-        #       read up to the last byte, but NOT past it.
-        #
-        # IMPLIES: if will_close is FALSE, then self.close() will ALWAYS be
-        #          called, meaning self.isclosed() is meaningful.
-        return self.fp is None
-
-    def read(self, amt=None):
-        if self.fp is None:
-            return b""
-
-        if self._method == "HEAD":
-            self._close_conn()
-            return b""
-
-        if amt is not None:
-            # Amount is given, so call base class version
-            # (which is implemented in terms of self.readinto)
-            return super(HTTPResponse, self).read(amt)
-        else:
-            # Amount is not given (unbounded read) so we must check self.length
-            # and self.chunked
-
-            if self.chunked:
-                return self._readall_chunked()
-
-            if self.length is None:
-                s = self.fp.read()
-            else:
-                try:
-                    s = self._safe_read(self.length)
-                except IncompleteRead:
-                    self._close_conn()
-                    raise
-                self.length = 0
-            self._close_conn()        # we read everything
-            return s
-
-    def readinto(self, b):
-        if self.fp is None:
-            return 0
-
-        if self._method == "HEAD":
-            self._close_conn()
-            return 0
-
-        if self.chunked:
-            return self._readinto_chunked(b)
-
-        if self.length is not None:
-            if len(b) > self.length:
-                # clip the read to the "end of response"
-                b = memoryview(b)[0:self.length]
-
-        # we do not use _safe_read() here because this may be a .will_close
-        # connection, and the user is reading more bytes than will be provided
-        # (for example, reading in 1k chunks)
-        n = self.fp.readinto(b)
-        if not n:
-            # Ideally, we would raise IncompleteRead if the content-length
-            # wasn't satisfied, but it might break compatibility.
-            self._close_conn()
-        elif self.length is not None:
-            self.length -= n
-            if not self.length:
-                self._close_conn()
-        return n
-
-    def _read_next_chunk_size(self):
-        # Read the next chunk size from the file
-        line = self.fp.readline(_MAXLINE + 1)
-        if len(line) > _MAXLINE:
-            raise LineTooLong("chunk size")
-        i = line.find(b";")
-        if i >= 0:
-            line = line[:i] # strip chunk-extensions
-        try:
-            return int(line, 16)
-        except ValueError:
-            # close the connection as protocol synchronisation is
-            # probably lost
-            self._close_conn()
-            raise
-
-    def _read_and_discard_trailer(self):
-        # read and discard trailer up to the CRLF terminator
-        ### note: we shouldn't have any trailers!
-        while True:
-            line = self.fp.readline(_MAXLINE + 1)
-            if len(line) > _MAXLINE:
-                raise LineTooLong("trailer line")
-            if not line:
-                # a vanishingly small number of sites EOF without
-                # sending the trailer
-                break
-            if line in (b'\r\n', b'\n', b''):
-                break
-
-    def _readall_chunked(self):
-        assert self.chunked != _UNKNOWN
-        chunk_left = self.chunk_left
-        value = []
-        while True:
-            if chunk_left is None:
-                try:
-                    chunk_left = self._read_next_chunk_size()
-                    if chunk_left == 0:
-                        break
-                except ValueError:
-                    raise IncompleteRead(b''.join(value))
-            value.append(self._safe_read(chunk_left))
-
-            # we read the whole chunk, get another
-            self._safe_read(2)      # toss the CRLF at the end of the chunk
-            chunk_left = None
-
-        self._read_and_discard_trailer()
-
-        # we read everything; close the "file"
-        self._close_conn()
-
-        return b''.join(value)
-
-    def _readinto_chunked(self, b):
-        assert self.chunked != _UNKNOWN
-        chunk_left = self.chunk_left
-
-        total_bytes = 0
-        mvb = memoryview(b)
-        while True:
-            if chunk_left is None:
-                try:
-                    chunk_left = self._read_next_chunk_size()
-                    if chunk_left == 0:
-                        break
-                except ValueError:
-                    raise IncompleteRead(bytes(b[0:total_bytes]))
-
-            if len(mvb) < chunk_left:
-                n = self._safe_readinto(mvb)
-                self.chunk_left = chunk_left - n
-                return total_bytes + n
-            elif len(mvb) == chunk_left:
-                n = self._safe_readinto(mvb)
-                self._safe_read(2)  # toss the CRLF at the end of the chunk
-                self.chunk_left = None
-                return total_bytes + n
-            else:
-                temp_mvb = mvb[0:chunk_left]
-                n = self._safe_readinto(temp_mvb)
-                mvb = mvb[n:]
-                total_bytes += n
-
-            # we read the whole chunk, get another
-            self._safe_read(2)      # toss the CRLF at the end of the chunk
-            chunk_left = None
-
-        self._read_and_discard_trailer()
-
-        # we read everything; close the "file"
-        self._close_conn()
-
-        return total_bytes
-
-    def _safe_read(self, amt):
-        """Read the number of bytes requested, compensating for partial reads.
-
-        Normally, we have a blocking socket, but a read() can be interrupted
-        by a signal (resulting in a partial read).
-
-        Note that we cannot distinguish between EOF and an interrupt when zero
-        bytes have been read. IncompleteRead() will be raised in this
-        situation.
-
-        This function should be used when <amt> bytes "should" be present for
-        reading. If the bytes are truly not available (due to EOF), then the
-        IncompleteRead exception can be used to detect the problem.
-        """
-        s = []
-        while amt > 0:
-            chunk = self.fp.read(min(amt, MAXAMOUNT))
-            if not chunk:
-                raise IncompleteRead(b''.join(s), amt)
-            s.append(chunk)
-            amt -= len(chunk)
-        return b"".join(s)
-
-    def _safe_readinto(self, b):
-        """Same as _safe_read, but for reading into a buffer."""
-        total_bytes = 0
-        mvb = memoryview(b)
-        while total_bytes < len(b):
-            if MAXAMOUNT < len(mvb):
-                temp_mvb = mvb[0:MAXAMOUNT]
-                n = self.fp.readinto(temp_mvb)
-            else:
-                n = self.fp.readinto(mvb)
-            if not n:
-                raise IncompleteRead(bytes(mvb[0:total_bytes]), len(b))
-            mvb = mvb[n:]
-            total_bytes += n
-        return total_bytes
-
-    def fileno(self):
-        return self.fp.fileno()
-
-    def getheader(self, name, default=None):
-        if self.headers is None:
-            raise ResponseNotReady()
-        headers = self.headers.get_all(name) or default
-        if isinstance(headers, str) or not hasattr(headers, '__iter__'):
-            return headers
-        else:
-            return ', '.join(headers)
-
-    def getheaders(self):
-        """Return list of (header, value) tuples."""
-        if self.headers is None:
-            raise ResponseNotReady()
-        return list(self.headers.items())
-
-    # We override IOBase.__iter__ so that it doesn't check for closed-ness
-
-    def __iter__(self):
-        return self
-
-    # For compatibility with old-style urllib responses.
-
-    def info(self):
-        return self.headers
-
-    def geturl(self):
-        return self.url
-
-    def getcode(self):
-        return self.status
-
-class HTTPConnection:
-
-    _http_vsn = 11
-    _http_vsn_str = 'HTTP/1.1'
-
-    response_class = HTTPResponse
-    default_port = HTTP_PORT
-    auto_open = 1
-    debuglevel = 0
-
-    def __init__(self, host, port=None, strict=_strict_sentinel,
-                 timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None):
-        if strict is not _strict_sentinel:
-            warnings.warn("the 'strict' argument isn't supported anymore; "
-                "http.client now always assumes HTTP/1.x compliant servers.",
-                DeprecationWarning, 2)
-        self.timeout = timeout
-        self.source_address = source_address
-        self.sock = None
-        self._buffer = []
-        self.__response = None
-        self.__state = _CS_IDLE
-        self._method = None
-        self._tunnel_host = None
-        self._tunnel_port = None
-        self._tunnel_headers = {}
-
-        self._set_hostport(host, port)
-
-    def set_tunnel(self, host, port=None, headers=None):
-        """ Sets up the host and the port for the HTTP CONNECT Tunnelling.
-
-        The headers argument should be a mapping of extra HTTP headers
-        to send with the CONNECT request.
-        """
-        self._tunnel_host = host
-        self._tunnel_port = port
-        if headers:
-            self._tunnel_headers = headers
-        else:
-            self._tunnel_headers.clear()
-
-    def _set_hostport(self, host, port):
-        if port is None:
-            i = host.rfind(':')
-            j = host.rfind(']')         # ipv6 addresses have [...]
-            if i > j:
-                try:
-                    port = int(host[i+1:])
-                except ValueError:
-                    if host[i+1:] == "": # http://foo.com:/ == http://foo.com/
-                        port = self.default_port
-                    else:
-                        raise InvalidURL("nonnumeric port: '%s'" % host[i+1:])
-                host = host[:i]
-            else:
-                port = self.default_port
-            if host and host[0] == '[' and host[-1] == ']':
-                host = host[1:-1]
-        self.host = host
-        self.port = port
-
-    def set_debuglevel(self, level):
-        self.debuglevel = level
-
-    def _tunnel(self):
-        self._set_hostport(self._tunnel_host, self._tunnel_port)
-        connect_str = "CONNECT %s:%d HTTP/1.0\r\n" % (self.host, self.port)
-        connect_bytes = connect_str.encode("ascii")
-        self.send(connect_bytes)
-        for header, value in self._tunnel_headers.items():
-            header_str = "%s: %s\r\n" % (header, value)
-            header_bytes = header_str.encode("latin-1")
-            self.send(header_bytes)
-        self.send(b'\r\n')
-
-        response = self.response_class(self.sock, method=self._method)
-        (version, code, message) = response._read_status()
-
-        if code != 200:
-            self.close()
-            raise socket.error("Tunnel connection failed: %d %s" % (code,
-                                                                    message.strip()))
-        while True:
-            line = response.fp.readline(_MAXLINE + 1)
-            if len(line) > _MAXLINE:
-                raise LineTooLong("header line")
-            if not line:
-                # for sites which EOF without sending a trailer
-                break
-            if line in (b'\r\n', b'\n', b''):
-                break
-
-    def connect(self):
-        """Connect to the host and port specified in __init__."""
-        self.sock = socket.create_connection((self.host,self.port),
-                                             self.timeout, self.source_address)
-        if self._tunnel_host:
-            self._tunnel()
-
-    def close(self):
-        """Close the connection to the HTTP server."""
-        if self.sock:
-            self.sock.close()   # close it manually... there may be other refs
-            self.sock = None
-        if self.__response:
-            self.__response.close()
-            self.__response = None
-        self.__state = _CS_IDLE
-
-    def send(self, data):
-        """Send `data' to the server.
-        ``data`` can be a string object, a bytes object, an array object, a
-        file-like object that supports a .read() method, or an iterable object.
-        """
-
-        if self.sock is None:
-            if self.auto_open:
-                self.connect()
-            else:
-                raise NotConnected()
-
-        if self.debuglevel > 0:
-            print("send:", repr(data))
-        blocksize = 8192
-        if hasattr(data, "read") :
-            if self.debuglevel > 0:
-                print("sendIng a read()able")
-            encode = False
-            try:
-                mode = data.mode
-            except AttributeError:
-                # io.BytesIO and other file-like objects don't have a `mode`
-                # attribute.
-                pass
-            else:
-                if "b" not in mode:
-                    encode = True
-                    if self.debuglevel > 0:
-                        print("encoding file using iso-8859-1")
-            while 1:
-                datablock = data.read(blocksize)
-                if not datablock:
-                    break
-                if encode:
-                    datablock = datablock.encode("iso-8859-1")
-                self.sock.sendall(datablock)
-            return
-        try:
-            self.sock.sendall(data)
-        except TypeError:
-            if isinstance(data, collections.Iterable):
-                for d in data:
-                    self.sock.sendall(d)
-            else:
-                raise TypeError("data should be a bytes-like object "
-                                "or an iterable, got %r" % type(data))
-
-    def _output(self, s):
-        """Add a line of output to the current request buffer.
-
-        Assumes that the line does *not* end with \\r\\n.
-        """
-        self._buffer.append(s)
-
-    def _send_output(self, message_body=None):
-        """Send the currently buffered request and clear the buffer.
-
-        Appends an extra \\r\\n to the buffer.
-        A message_body may be specified, to be appended to the request.
-        """
-        self._buffer.extend((b"", b""))
-        msg = b"\r\n".join(self._buffer)
-        del self._buffer[:]
-        # If msg and message_body are sent in a single send() call,
-        # it will avoid performance problems caused by the interaction
-        # between delayed ack and the Nagle algorithm.
-        if isinstance(message_body, bytes):
-            msg += message_body
-            message_body = None
-        self.send(msg)
-        if message_body is not None:
-            # message_body was not a string (i.e. it is a file), and
-            # we must run the risk of Nagle.
-            self.send(message_body)
-
-    def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0):
-        """Send a request to the server.
-
-        `method' specifies an HTTP request method, e.g. 'GET'.
-        `url' specifies the object being requested, e.g. '/index.html'.
-        `skip_host' if True does not add automatically a 'Host:' header
-        `skip_accept_encoding' if True does not add automatically an
-           'Accept-Encoding:' header
-        """
-
-        # if a prior response has been completed, then forget about it.
-        if self.__response and self.__response.isclosed():
-            self.__response = None
-
-
-        # in certain cases, we cannot issue another request on this connection.
-        # this occurs when:
-        #   1) we are in the process of sending a request.   (_CS_REQ_STARTED)
-        #   2) a response to a previous request has signalled that it is going
-        #      to close the connection upon completion.
-        #   3) the headers for the previous response have not been read, thus
-        #      we cannot determine whether point (2) is true.   (_CS_REQ_SENT)
-        #
-        # if there is no prior response, then we can request at will.
-        #
-        # if point (2) is true, then we will have passed the socket to the
-        # response (effectively meaning, "there is no prior response"), and
-        # will open a new one when a new request is made.
-        #
-        # Note: if a prior response exists, then we *can* start a new request.
-        #       We are not allowed to begin fetching the response to this new
-        #       request, however, until that prior response is complete.
-        #
-        if self.__state == _CS_IDLE:
-            self.__state = _CS_REQ_STARTED
-        else:
-            raise CannotSendRequest(self.__state)
-
-        # Save the method we use, we need it later in the response phase
-        self._method = method
-        if not url:
-            url = '/'
-        request = '%s %s %s' % (method, url, self._http_vsn_str)
-
-        # Non-ASCII characters should have been eliminated earlier
-        self._output(request.encode('ascii'))
-
-        if self._http_vsn == 11:
-            # Issue some standard headers for better HTTP/1.1 compliance
-
-            if not skip_host:
-                # this header is issued *only* for HTTP/1.1
-                # connections. more specifically, this means it is
-                # only issued when the client uses the new
-                # HTTPConnection() class. backwards-compat clients
-                # will be using HTTP/1.0 and those clients may be
-                # issuing this header themselves. we should NOT issue
-                # it twice; some web servers (such as Apache) barf
-                # when they see two Host: headers
-
-                # If we need a non-standard port,include it in the
-                # header.  If the request is going through a proxy,
-                # but the host of the actual URL, not the host of the
-                # proxy.
-
-                netloc = ''
-                if url.startswith('http'):
-                    nil, netloc, nil, nil, nil = urlsplit(url)
-
-                if netloc:
-                    try:
-                        netloc_enc = netloc.encode("ascii")
-                    except UnicodeEncodeError:
-                        netloc_enc = netloc.encode("idna")
-                    self.putheader('Host', netloc_enc)
-                else:
-                    try:
-                        host_enc = self.host.encode("ascii")
-                    except UnicodeEncodeError:
-                        host_enc = self.host.encode("idna")
-
-                    # As per RFC 273, IPv6 address should be wrapped with []
-                    # when used as Host header
-
-                    if self.host.find(':') >= 0:
-                        host_enc = b'[' + host_enc + b']'
-
-                    if self.port == self.default_port:
-                        self.putheader('Host', host_enc)
-                    else:
-                        host_enc = host_enc.decode("ascii")
-                        self.putheader('Host', "%s:%s" % (host_enc, self.port))
-
-            # note: we are assuming that clients will not attempt to set these
-            #       headers since *this* library must deal with the
-            #       consequences. this also means that when the supporting
-            #       libraries are updated to recognize other forms, then this
-            #       code should be changed (removed or updated).
-
-            # we only want a Content-Encoding of "identity" since we don't
-            # support encodings such as x-gzip or x-deflate.
-            if not skip_accept_encoding:
-                self.putheader('Accept-Encoding', 'identity')
-
-            # we can accept "chunked" Transfer-Encodings, but no others
-            # NOTE: no TE header implies *only* "chunked"
-            #self.putheader('TE', 'chunked')
-
-            # if TE is supplied in the header, then it must appear in a
-            # Connection header.
-            #self.putheader('Connection', 'TE')
-
-        else:
-            # For HTTP/1.0, the server will assume "not chunked"
-            pass
-
-    def putheader(self, header, *values):
-        """Send a request header line to the server.
-
-        For example: h.putheader('Accept', 'text/html')
-        """
-        if self.__state != _CS_REQ_STARTED:
-            raise CannotSendHeader()
-
-        if hasattr(header, 'encode'):
-            header = header.encode('ascii')
-        values = list(values)
-        for i, one_value in enumerate(values):
-            if isinstance(one_value, str):
-                values[i] = one_value.encode('latin-1')
-            elif isinstance(one_value, int):
-                values[i] = str(one_value).encode('ascii')
-        value = b'\r\n\t'.join(values)
-        header = header + b': ' + value
-        self._output(header)
-
-    def endheaders(self, message_body=None):
-        """Indicate that the last header line has been sent to the server.
-
-        This method sends the request to the server.  The optional message_body
-        argument can be used to pass a message body associated with the
-        request.  The message body will be sent in the same packet as the
-        message headers if it is a string, otherwise it is sent as a separate
-        packet.
-        """
-        if self.__state == _CS_REQ_STARTED:
-            self.__state = _CS_REQ_SENT
-        else:
-            raise CannotSendHeader()
-        self._send_output(message_body)
-
-    def request(self, method, url, body=None, headers={}):
-        """Send a complete request to the server."""
-        self._send_request(method, url, body, headers)
-
-    def _set_content_length(self, body):
-        # Set the content-length based on the body.
-        thelen = None
-        try:
-            thelen = str(len(body))
-        except TypeError as te:
-            # If this is a file-like object, try to
-            # fstat its file descriptor
-            try:
-                thelen = str(os.fstat(body.fileno()).st_size)
-            except (AttributeError, OSError):
-                # Don't send a length if this failed
-                if self.debuglevel > 0: print("Cannot stat!!")
-
-        if thelen is not None:
-            self.putheader('Content-Length', thelen)
-
-    def _send_request(self, method, url, body, headers):
-        # Honor explicitly requested Host: and Accept-Encoding: headers.
-        header_names = dict.fromkeys([k.lower() for k in headers])
-        skips = {}
-        if 'host' in header_names:
-            skips['skip_host'] = 1
-        if 'accept-encoding' in header_names:
-            skips['skip_accept_encoding'] = 1
-
-        self.putrequest(method, url, **skips)
-
-        if body is not None and ('content-length' not in header_names):
-            self._set_content_length(body)
-        for hdr, value in headers.items():
-            self.putheader(hdr, value)
-        if isinstance(body, str):
-            # RFC 2616 Section 3.7.1 says that text default has a
-            # default charset of iso-8859-1.
-            body = body.encode('iso-8859-1')
-        self.endheaders(body)
-
-    def getresponse(self):
-        """Get the response from the server.
-
-        If the HTTPConnection is in the correct state, returns an
-        instance of HTTPResponse or of whatever object is returned by
-        class the response_class variable.
-
-        If a request has not been sent or if a previous response has
-        not be handled, ResponseNotReady is raised.  If the HTTP
-        response indicates that the connection should be closed, then
-        it will be closed before the response is returned.  When the
-        connection is closed, the underlying socket is closed.
-        """
-
-        # if a prior response has been completed, then forget about it.
-        if self.__response and self.__response.isclosed():
-            self.__response = None
-
-        # if a prior response exists, then it must be completed (otherwise, we
-        # cannot read this response's header to determine the connection-close
-        # behavior)
-        #
-        # note: if a prior response existed, but was connection-close, then the
-        # socket and response were made independent of this HTTPConnection
-        # object since a new request requires that we open a whole new
-        # connection
-        #
-        # this means the prior response had one of two states:
-        #   1) will_close: this connection was reset and the prior socket and
-        #                  response operate independently
-        #   2) persistent: the response was retained and we await its
-        #                  isclosed() status to become true.
-        #
-        if self.__state != _CS_REQ_SENT or self.__response:
-            raise ResponseNotReady(self.__state)
-
-        if self.debuglevel > 0:
-            response = self.response_class(self.sock, self.debuglevel,
-                                           method=self._method)
-        else:
-            response = self.response_class(self.sock, method=self._method)
-
-        response.begin()
-        assert response.will_close != _UNKNOWN
-        self.__state = _CS_IDLE
-
-        if response.will_close:
-            # this effectively passes the connection to the response
-            self.close()
-        else:
-            # remember this, so we can tell when it is complete
-            self.__response = response
-
-        return response
-
-try:
-    import ssl
-except ImportError:
-    pass
-else:
-    class HTTPSConnection(HTTPConnection):
-        "This class allows communication via SSL."
-
-        default_port = HTTPS_PORT
-
-        # XXX Should key_file and cert_file be deprecated in favour of context?
-
-        def __init__(self, host, port=None, key_file=None, cert_file=None,
-                     strict=_strict_sentinel, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
-                     source_address=None, *, context=None, check_hostname=None):
-            super(HTTPSConnection, self).__init__(host, port, strict, timeout,
-                                                  source_address)
-            self.key_file = key_file
-            self.cert_file = cert_file
-            if context is None:
-                # Some reasonable defaults
-                context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
-                context.options |= ssl.OP_NO_SSLv2
-            will_verify = context.verify_mode != ssl.CERT_NONE
-            if check_hostname is None:
-                check_hostname = will_verify
-            elif check_hostname and not will_verify:
-                raise ValueError("check_hostname needs a SSL context with "
-                                 "either CERT_OPTIONAL or CERT_REQUIRED")
-            if key_file or cert_file:
-                context.load_cert_chain(cert_file, key_file)
-            self._context = context
-            self._check_hostname = check_hostname
-
-        def connect(self):
-            "Connect to a host on a given (SSL) port."
-
-            sock = socket.create_connection((self.host, self.port),
-                                            self.timeout, self.source_address)
-
-            if self._tunnel_host:
-                self.sock = sock
-                self._tunnel()
-
-            server_hostname = self.host if ssl.HAS_SNI else None
-            self.sock = self._context.wrap_socket(sock,
-                                                  server_hostname=server_hostname)
-            try:
-                if self._check_hostname:
-                    ssl.match_hostname(self.sock.getpeercert(), self.host)
-            except Exception:
-                self.sock.shutdown(socket.SHUT_RDWR)
-                self.sock.close()
-                raise
-
-    __all__.append("HTTPSConnection")
-
-class HTTPException(Exception):
-    # Subclasses that define an __init__ must call Exception.__init__
-    # or define self.args.  Otherwise, str() will fail.
-    pass
-
-class NotConnected(HTTPException):
-    pass
-
-class InvalidURL(HTTPException):
-    pass
-
-class UnknownProtocol(HTTPException):
-    def __init__(self, version):
-        self.args = version,
-        self.version = version
-
-class UnknownTransferEncoding(HTTPException):
-    pass
-
-class UnimplementedFileMode(HTTPException):
-    pass
-
-class IncompleteRead(HTTPException):
-    def __init__(self, partial, expected=None):
-        self.args = partial,
-        self.partial = partial
-        self.expected = expected
-    def __repr__(self):
-        if self.expected is not None:
-            e = ', %i more expected' % self.expected
-        else:
-            e = ''
-        return 'IncompleteRead(%i bytes read%s)' % (len(self.partial), e)
-    def __str__(self):
-        return repr(self)
-
-class ImproperConnectionState(HTTPException):
-    pass
-
-class CannotSendRequest(ImproperConnectionState):
-    pass
-
-class CannotSendHeader(ImproperConnectionState):
-    pass
-
-class ResponseNotReady(ImproperConnectionState):
-    pass
-
-class BadStatusLine(HTTPException):
-    def __init__(self, line):
-        if not line:
-            line = repr(line)
-        self.args = line,
-        self.line = line
-
-class LineTooLong(HTTPException):
-    def __init__(self, line_type):
-        HTTPException.__init__(self, "got more than %d bytes when reading %s"
-                                     % (_MAXLINE, line_type))
-
-# for backwards compatibility
-error = HTTPException
diff --git a/http.client/metadata.txt b/http.client/metadata.txt
deleted file mode 100644
index f4129e173..000000000
--- a/http.client/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5
-depends = email.parser, email.message, socket, collections, urllib.parse, warnings
diff --git a/http.client/setup.py b/http.client/setup.py
deleted file mode 100644
index e138156fd..000000000
--- a/http.client/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-http.client',
-      version='0.5',
-      description='CPython http.client module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['http'],
-      install_requires=['micropython-email.parser', 'micropython-email.message', 'micropython-socket', 'micropython-collections', 'micropython-urllib.parse', 'micropython-warnings'])
diff --git a/http.client/test_client.py b/http.client/test_client.py
deleted file mode 100644
index f2800dd59..000000000
--- a/http.client/test_client.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from http.client import HTTPConnection
-
-
-conn = HTTPConnection("localhost")
-#conn = HTTPConnection("python.org")
-conn.request("GET", "/")
-resp = conn.getresponse()
-print(resp)
-print(resp.read())
diff --git a/imaplib/imaplib.py b/imaplib/imaplib.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/imaplib/metadata.txt b/imaplib/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/imaplib/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/imaplib/setup.py b/imaplib/setup.py
deleted file mode 100644
index b246435dd..000000000
--- a/imaplib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-imaplib',
-      version='0.0.0',
-      description='Dummy imaplib module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['imaplib'])
diff --git a/imp/imp.py b/imp/imp.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/imp/metadata.txt b/imp/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/imp/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/imp/setup.py b/imp/setup.py
deleted file mode 100644
index 734761300..000000000
--- a/imp/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-imp',
-      version='0.0.0',
-      description='Dummy imp module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['imp'])
diff --git a/inspect/metadata.txt b/inspect/metadata.txt
deleted file mode 100644
index a90b2ef38..000000000
--- a/inspect/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1.1
diff --git a/inspect/setup.py b/inspect/setup.py
deleted file mode 100644
index 8581970b9..000000000
--- a/inspect/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-inspect',
-      version='0.1.1',
-      description='inspect module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['inspect'])
diff --git a/io/io.py b/io/io.py
deleted file mode 100644
index 5536a308c..000000000
--- a/io/io.py
+++ /dev/null
@@ -1 +0,0 @@
-from _io import *
diff --git a/io/metadata.txt b/io/metadata.txt
deleted file mode 100644
index dc5f60a66..000000000
--- a/io/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.1
diff --git a/io/setup.py b/io/setup.py
deleted file mode 100644
index a28fccba7..000000000
--- a/io/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-io',
-      version='0.0.1',
-      description='Dummy io module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['io'])
diff --git a/ipaddress/ipaddress.py b/ipaddress/ipaddress.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ipaddress/metadata.txt b/ipaddress/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/ipaddress/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/ipaddress/setup.py b/ipaddress/setup.py
deleted file mode 100644
index bcbf5ccf9..000000000
--- a/ipaddress/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-ipaddress',
-      version='0.0.0',
-      description='Dummy ipaddress module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['ipaddress'])
diff --git a/itertools/metadata.txt b/itertools/metadata.txt
deleted file mode 100644
index 9ecea5da7..000000000
--- a/itertools/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.2.1
diff --git a/itertools/setup.py b/itertools/setup.py
deleted file mode 100644
index 65af5f1f4..000000000
--- a/itertools/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-itertools',
-      version='0.2.1',
-      description='itertools module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['itertools'])
diff --git a/itertools/test_itertools.py b/itertools/test_itertools.py
deleted file mode 100644
index 6a4ad1be1..000000000
--- a/itertools/test_itertools.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import itertools
-
-assert list(itertools.islice(list(range(10)), 4)) == [0, 1, 2, 3]
-assert list(itertools.islice(list(range(10)), 2, 6)) == [2, 3, 4, 5]
-assert list(itertools.islice(list(range(10)), 2, 6, 2)) == [2, 4]
-
-def g():
-    while True:
-        yield 123
-
-assert list(itertools.islice(g(), 5)) == [123, 123, 123, 123, 123]
-
-assert list(itertools.islice(itertools.cycle([1, 2, 3]), 10)) == [1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
-assert list(itertools.islice(itertools.cycle(reversed([1, 2, 3])), 7)) == [3, 2, 1, 3, 2, 1, 3]
-
-assert list(itertools.starmap(lambda x, y: x * y, [[1, 2], [2, 3], [3, 4]])) == [2, 6, 12]
diff --git a/json/json/__init__.py b/json/json/__init__.py
deleted file mode 100644
index 48a4f8f86..000000000
--- a/json/json/__init__.py
+++ /dev/null
@@ -1,332 +0,0 @@
-r"""JSON (JavaScript Object Notation) <http://json.org> is a subset of
-JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data
-interchange format.
-
-:mod:`json` exposes an API familiar to users of the standard library
-:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained
-version of the :mod:`json` library contained in Python 2.6, but maintains
-compatibility with Python 2.4 and Python 2.5 and (currently) has
-significant performance advantages, even without using the optional C
-extension for speedups.
-
-Encoding basic Python object hierarchies::
-
-    >>> import json
-    >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
-    '["foo", {"bar": ["baz", null, 1.0, 2]}]'
-    >>> print(json.dumps("\"foo\bar"))
-    "\"foo\bar"
-    >>> print(json.dumps('\u1234'))
-    "\u1234"
-    >>> print(json.dumps('\\'))
-    "\\"
-    >>> print(json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True))
-    {"a": 0, "b": 0, "c": 0}
-    >>> from io import StringIO
-    >>> io = StringIO()
-    >>> json.dump(['streaming API'], io)
-    >>> io.getvalue()
-    '["streaming API"]'
-
-Compact encoding::
-
-    >>> import json
-    >>> from collections import OrderedDict
-    >>> mydict = OrderedDict([('4', 5), ('6', 7)])
-    >>> json.dumps([1,2,3,mydict], separators=(',', ':'))
-    '[1,2,3,{"4":5,"6":7}]'
-
-Pretty printing::
-
-    >>> import json
-    >>> print(json.dumps({'4': 5, '6': 7}, sort_keys=True,
-    ...                  indent=4, separators=(',', ': ')))
-    {
-        "4": 5,
-        "6": 7
-    }
-
-Decoding JSON::
-
-    >>> import json
-    >>> obj = ['foo', {'bar': ['baz', None, 1.0, 2]}]
-    >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj
-    True
-    >>> json.loads('"\\"foo\\bar"') == '"foo\x08ar'
-    True
-    >>> from io import StringIO
-    >>> io = StringIO('["streaming API"]')
-    >>> json.load(io)[0] == 'streaming API'
-    True
-
-Specializing JSON object decoding::
-
-    >>> import json
-    >>> def as_complex(dct):
-    ...     if '__complex__' in dct:
-    ...         return complex(dct['real'], dct['imag'])
-    ...     return dct
-    ...
-    >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}',
-    ...     object_hook=as_complex)
-    (1+2j)
-    >>> from decimal import Decimal
-    >>> json.loads('1.1', parse_float=Decimal) == Decimal('1.1')
-    True
-
-Specializing JSON object encoding::
-
-    >>> import json
-    >>> def encode_complex(obj):
-    ...     if isinstance(obj, complex):
-    ...         return [obj.real, obj.imag]
-    ...     raise TypeError(repr(o) + " is not JSON serializable")
-    ...
-    >>> json.dumps(2 + 1j, default=encode_complex)
-    '[2.0, 1.0]'
-    >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j)
-    '[2.0, 1.0]'
-    >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j))
-    '[2.0, 1.0]'
-
-
-Using json.tool from the shell to validate and pretty-print::
-
-    $ echo '{"json":"obj"}' | python -m json.tool
-    {
-        "json": "obj"
-    }
-    $ echo '{ 1.2:3.4}' | python -m json.tool
-    Expecting property name enclosed in double quotes: line 1 column 3 (char 2)
-"""
-__version__ = '2.0.9'
-__all__ = [
-    'dump', 'dumps', 'load', 'loads',
-    'JSONDecoder', 'JSONEncoder',
-]
-
-__author__ = 'Bob Ippolito <bob@redivi.com>'
-
-from .decoder import JSONDecoder
-from .encoder import JSONEncoder
-
-_default_encoder = JSONEncoder(
-    skipkeys=False,
-    ensure_ascii=True,
-    check_circular=True,
-    allow_nan=True,
-    indent=None,
-    separators=None,
-    default=None,
-)
-
-def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
-        allow_nan=True, cls=None, indent=None, separators=None,
-        default=None, sort_keys=False, **kw):
-    """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a
-    ``.write()``-supporting file-like object).
-
-    If ``skipkeys`` is true then ``dict`` keys that are not basic types
-    (``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped
-    instead of raising a ``TypeError``.
-
-    If ``ensure_ascii`` is false, then the strings written to ``fp`` can
-    contain non-ASCII characters if they appear in strings contained in
-    ``obj``. Otherwise, all such characters are escaped in JSON strings.
-
-    If ``check_circular`` is false, then the circular reference check
-    for container types will be skipped and a circular reference will
-    result in an ``OverflowError`` (or worse).
-
-    If ``allow_nan`` is false, then it will be a ``ValueError`` to
-    serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``)
-    in strict compliance of the JSON specification, instead of using the
-    JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
-
-    If ``indent`` is a non-negative integer, then JSON array elements and
-    object members will be pretty-printed with that indent level. An indent
-    level of 0 will only insert newlines. ``None`` is the most compact
-    representation.  Since the default item separator is ``', '``,  the
-    output might include trailing whitespace when ``indent`` is specified.
-    You can use ``separators=(',', ': ')`` to avoid this.
-
-    If ``separators`` is an ``(item_separator, dict_separator)`` tuple
-    then it will be used instead of the default ``(', ', ': ')`` separators.
-    ``(',', ':')`` is the most compact JSON representation.
-
-    ``default(obj)`` is a function that should return a serializable version
-    of obj or raise TypeError. The default simply raises TypeError.
-
-    If *sort_keys* is ``True`` (default: ``False``), then the output of
-    dictionaries will be sorted by key.
-
-    To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the
-    ``.default()`` method to serialize additional types), specify it with
-    the ``cls`` kwarg; otherwise ``JSONEncoder`` is used.
-
-    """
-    # cached encoder
-    if (not skipkeys and ensure_ascii and
-        check_circular and allow_nan and
-        cls is None and indent is None and separators is None and
-        default is None and not sort_keys and not kw):
-        iterable = _default_encoder.iterencode(obj)
-    else:
-        if cls is None:
-            cls = JSONEncoder
-        iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii,
-            check_circular=check_circular, allow_nan=allow_nan, indent=indent,
-            separators=separators,
-            default=default, sort_keys=sort_keys, **kw).iterencode(obj)
-    # could accelerate with writelines in some versions of Python, at
-    # a debuggability cost
-    for chunk in iterable:
-        fp.write(chunk)
-
-
-def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
-        allow_nan=True, cls=None, indent=None, separators=None,
-        default=None, sort_keys=False, **kw):
-    """Serialize ``obj`` to a JSON formatted ``str``.
-
-    If ``skipkeys`` is false then ``dict`` keys that are not basic types
-    (``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped
-    instead of raising a ``TypeError``.
-
-    If ``ensure_ascii`` is false, then the return value can contain non-ASCII
-    characters if they appear in strings contained in ``obj``. Otherwise, all
-    such characters are escaped in JSON strings.
-
-    If ``check_circular`` is false, then the circular reference check
-    for container types will be skipped and a circular reference will
-    result in an ``OverflowError`` (or worse).
-
-    If ``allow_nan`` is false, then it will be a ``ValueError`` to
-    serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in
-    strict compliance of the JSON specification, instead of using the
-    JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
-
-    If ``indent`` is a non-negative integer, then JSON array elements and
-    object members will be pretty-printed with that indent level. An indent
-    level of 0 will only insert newlines. ``None`` is the most compact
-    representation.  Since the default item separator is ``', '``,  the
-    output might include trailing whitespace when ``indent`` is specified.
-    You can use ``separators=(',', ': ')`` to avoid this.
-
-    If ``separators`` is an ``(item_separator, dict_separator)`` tuple
-    then it will be used instead of the default ``(', ', ': ')`` separators.
-    ``(',', ':')`` is the most compact JSON representation.
-
-    ``default(obj)`` is a function that should return a serializable version
-    of obj or raise TypeError. The default simply raises TypeError.
-
-    If *sort_keys* is ``True`` (default: ``False``), then the output of
-    dictionaries will be sorted by key.
-
-    To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the
-    ``.default()`` method to serialize additional types), specify it with
-    the ``cls`` kwarg; otherwise ``JSONEncoder`` is used.
-
-    """
-    # cached encoder
-    if (not skipkeys and ensure_ascii and
-        check_circular and allow_nan and
-        cls is None and indent is None and separators is None and
-        default is None and not sort_keys and not kw):
-        return _default_encoder.encode(obj)
-    if cls is None:
-        cls = JSONEncoder
-    return cls(
-        skipkeys=skipkeys, ensure_ascii=ensure_ascii,
-        check_circular=check_circular, allow_nan=allow_nan, indent=indent,
-        separators=separators, default=default, sort_keys=sort_keys,
-        **kw).encode(obj)
-
-
-_default_decoder = JSONDecoder(object_hook=None, object_pairs_hook=None)
-
-
-def load(fp, cls=None, object_hook=None, parse_float=None,
-        parse_int=None, parse_constant=None, object_pairs_hook=None, **kw):
-    """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing
-    a JSON document) to a Python object.
-
-    ``object_hook`` is an optional function that will be called with the
-    result of any object literal decode (a ``dict``). The return value of
-    ``object_hook`` will be used instead of the ``dict``. This feature
-    can be used to implement custom decoders (e.g. JSON-RPC class hinting).
-
-    ``object_pairs_hook`` is an optional function that will be called with the
-    result of any object literal decoded with an ordered list of pairs.  The
-    return value of ``object_pairs_hook`` will be used instead of the ``dict``.
-    This feature can be used to implement custom decoders that rely on the
-    order that the key and value pairs are decoded (for example,
-    collections.OrderedDict will remember the order of insertion). If
-    ``object_hook`` is also defined, the ``object_pairs_hook`` takes priority.
-
-    To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
-    kwarg; otherwise ``JSONDecoder`` is used.
-
-    """
-    return loads(fp.read(),
-        cls=cls, object_hook=object_hook,
-        parse_float=parse_float, parse_int=parse_int,
-        parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
-
-
-def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None,
-        parse_int=None, parse_constant=None, object_pairs_hook=None, **kw):
-    """Deserialize ``s`` (a ``str`` instance containing a JSON
-    document) to a Python object.
-
-    ``object_hook`` is an optional function that will be called with the
-    result of any object literal decode (a ``dict``). The return value of
-    ``object_hook`` will be used instead of the ``dict``. This feature
-    can be used to implement custom decoders (e.g. JSON-RPC class hinting).
-
-    ``object_pairs_hook`` is an optional function that will be called with the
-    result of any object literal decoded with an ordered list of pairs.  The
-    return value of ``object_pairs_hook`` will be used instead of the ``dict``.
-    This feature can be used to implement custom decoders that rely on the
-    order that the key and value pairs are decoded (for example,
-    collections.OrderedDict will remember the order of insertion). If
-    ``object_hook`` is also defined, the ``object_pairs_hook`` takes priority.
-
-    ``parse_float``, if specified, will be called with the string
-    of every JSON float to be decoded. By default this is equivalent to
-    float(num_str). This can be used to use another datatype or parser
-    for JSON floats (e.g. decimal.Decimal).
-
-    ``parse_int``, if specified, will be called with the string
-    of every JSON int to be decoded. By default this is equivalent to
-    int(num_str). This can be used to use another datatype or parser
-    for JSON integers (e.g. float).
-
-    ``parse_constant``, if specified, will be called with one of the
-    following strings: -Infinity, Infinity, NaN, null, true, false.
-    This can be used to raise an exception if invalid JSON numbers
-    are encountered.
-
-    To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
-    kwarg; otherwise ``JSONDecoder`` is used.
-
-    The ``encoding`` argument is ignored and deprecated.
-
-    """
-    if (cls is None and object_hook is None and
-            parse_int is None and parse_float is None and
-            parse_constant is None and object_pairs_hook is None and not kw):
-        return _default_decoder.decode(s)
-    if cls is None:
-        cls = JSONDecoder
-    if object_hook is not None:
-        kw['object_hook'] = object_hook
-    if object_pairs_hook is not None:
-        kw['object_pairs_hook'] = object_pairs_hook
-    if parse_float is not None:
-        kw['parse_float'] = parse_float
-    if parse_int is not None:
-        kw['parse_int'] = parse_int
-    if parse_constant is not None:
-        kw['parse_constant'] = parse_constant
-    return cls(**kw).decode(s)
diff --git a/json/json/scanner.py b/json/json/scanner.py
deleted file mode 100644
index 23eef61b9..000000000
--- a/json/json/scanner.py
+++ /dev/null
@@ -1,73 +0,0 @@
-"""JSON token scanner
-"""
-import re
-try:
-    from _json import make_scanner as c_make_scanner
-except ImportError:
-    c_make_scanner = None
-
-__all__ = ['make_scanner']
-
-NUMBER_RE = re.compile(
-    r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?',
-    (re.VERBOSE | re.MULTILINE | re.DOTALL))
-
-def py_make_scanner(context):
-    parse_object = context.parse_object
-    parse_array = context.parse_array
-    parse_string = context.parse_string
-    match_number = NUMBER_RE.match
-    strict = context.strict
-    parse_float = context.parse_float
-    parse_int = context.parse_int
-    parse_constant = context.parse_constant
-    object_hook = context.object_hook
-    object_pairs_hook = context.object_pairs_hook
-    memo = context.memo
-
-    def _scan_once(string, idx):
-        try:
-            nextchar = string[idx]
-        except IndexError:
-            raise StopIteration
-
-        if nextchar == '"':
-            return parse_string(string, idx + 1, strict)
-        elif nextchar == '{':
-            return parse_object((string, idx + 1), strict,
-                _scan_once, object_hook, object_pairs_hook, memo)
-        elif nextchar == '[':
-            return parse_array((string, idx + 1), _scan_once)
-        elif nextchar == 'n' and string[idx:idx + 4] == 'null':
-            return None, idx + 4
-        elif nextchar == 't' and string[idx:idx + 4] == 'true':
-            return True, idx + 4
-        elif nextchar == 'f' and string[idx:idx + 5] == 'false':
-            return False, idx + 5
-
-        m = match_number(string, idx)
-        if m is not None:
-            integer, frac, exp = m.groups()
-            if frac or exp:
-                res = parse_float(integer + (frac or '') + (exp or ''))
-            else:
-                res = parse_int(integer)
-            return res, m.end()
-        elif nextchar == 'N' and string[idx:idx + 3] == 'NaN':
-            return parse_constant('NaN'), idx + 3
-        elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity':
-            return parse_constant('Infinity'), idx + 8
-        elif nextchar == '-' and string[idx:idx + 9] == '-Infinity':
-            return parse_constant('-Infinity'), idx + 9
-        else:
-            raise StopIteration
-
-    def scan_once(string, idx):
-        try:
-            return _scan_once(string, idx)
-        finally:
-            memo.clear()
-
-    return _scan_once
-
-make_scanner = c_make_scanner or py_make_scanner
diff --git a/json/json/tool.py b/json/json/tool.py
deleted file mode 100644
index ecf9c4788..000000000
--- a/json/json/tool.py
+++ /dev/null
@@ -1,40 +0,0 @@
-r"""Command-line tool to validate and pretty-print JSON
-
-Usage::
-
-    $ echo '{"json":"obj"}' | python -m json.tool
-    {
-        "json": "obj"
-    }
-    $ echo '{ 1.2:3.4}' | python -m json.tool
-    Expecting property name enclosed in double quotes: line 1 column 3 (char 2)
-
-"""
-import sys
-import json
-
-def main():
-    if len(sys.argv) == 1:
-        infile = sys.stdin
-        outfile = sys.stdout
-    elif len(sys.argv) == 2:
-        infile = open(sys.argv[1], 'r')
-        outfile = sys.stdout
-    elif len(sys.argv) == 3:
-        infile = open(sys.argv[1], 'r')
-        outfile = open(sys.argv[2], 'w')
-    else:
-        raise SystemExit(sys.argv[0] + " [infile [outfile]]")
-    with infile:
-        try:
-            obj = json.load(infile)
-        except ValueError as e:
-            raise SystemExit(e)
-    with outfile:
-        json.dump(obj, outfile, sort_keys=True,
-                  indent=4, separators=(',', ': '))
-        outfile.write('\n')
-
-
-if __name__ == '__main__':
-    main()
diff --git a/json/setup.py b/json/setup.py
deleted file mode 100644
index 994479261..000000000
--- a/json/setup.py
+++ /dev/null
@@ -1,16 +0,0 @@
-#import sys
-# Remove current dir from sys.path, otherwise distutils will peek up our
-# copy module instead of system.
-#sys.path.pop(0)
-from setuptools import setup
-
-setup(name='micropython-json',
-      version='0.1',
-      description='CPython json package ported to MicroPython',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      install_requires=['micropython-re-pcre'],
-      packages=['json'])
diff --git a/json/test_json.py b/json/test_json.py
deleted file mode 100644
index 1fef2130c..000000000
--- a/json/test_json.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import json
-
-inp = ['foo', {'bar': ('baz', None, 1, 2)}]
-print(inp)
-
-s = json.dumps(inp)
-print(s)
-
-outp = json.loads(s)
-print(outp)
-
-# Doesn't work because JSON doesn't have tuples
-#assert inp == outp
diff --git a/keyword/keyword.py b/keyword/keyword.py
deleted file mode 100644
index 6ba185393..000000000
--- a/keyword/keyword.py
+++ /dev/null
@@ -1,94 +0,0 @@
-#! /usr/bin/env python3
-
-"""Keywords (from "graminit.c")
-
-This file is automatically generated; please don't muck it up!
-
-To update the symbols in this file, 'cd' to the top directory of
-the python source tree after building the interpreter and run:
-
-    ./python Lib/keyword.py
-"""
-
-__all__ = ["iskeyword", "kwlist"]
-
-kwlist = [
-#--start keywords--
-        'False',
-        'None',
-        'True',
-        'and',
-        'as',
-        'assert',
-        'break',
-        'class',
-        'continue',
-        'def',
-        'del',
-        'elif',
-        'else',
-        'except',
-        'finally',
-        'for',
-        'from',
-        'global',
-        'if',
-        'import',
-        'in',
-        'is',
-        'lambda',
-        'nonlocal',
-        'not',
-        'or',
-        'pass',
-        'raise',
-        'return',
-        'try',
-        'while',
-        'with',
-        'yield',
-#--end keywords--
-        ]
-
-frozenset = set
-iskeyword = frozenset(kwlist).__contains__
-
-def main():
-    import sys, re
-
-    args = sys.argv[1:]
-    iptfile = args and args[0] or "Python/graminit.c"
-    if len(args) > 1: optfile = args[1]
-    else: optfile = "Lib/keyword.py"
-
-    # scan the source file for keywords
-    with open(iptfile) as fp:
-        strprog = re.compile('"([^"]+)"')
-        lines = []
-        for line in fp:
-            if '{1, "' in line:
-                match = strprog.search(line)
-                if match:
-                    lines.append("        '" + match.group(1) + "',\n")
-    lines.sort()
-
-    # load the output skeleton from the target
-    with open(optfile) as fp:
-        format = fp.readlines()
-
-    # insert the lines of keywords
-    try:
-        start = format.index("#--start keywords--\n") + 1
-        end = format.index("#--end keywords--\n")
-        format[start:end] = lines
-    except ValueError:
-        sys.stderr.write("target does not contain format markers\n")
-        sys.exit(1)
-
-    # write the output file
-    fp = open(optfile, 'w')
-    fp.write(''.join(format))
-    fp.close()
-
-if __name__ == "__main__":
-    main()
diff --git a/locale/metadata.txt b/locale/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/locale/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/locale/setup.py b/locale/setup.py
deleted file mode 100644
index 3c53d2ef6..000000000
--- a/locale/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-locale',
-      version='0.0.1',
-      description='Dummy locale module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['locale'])
diff --git a/logging/example_logging.py b/logging/example_logging.py
deleted file mode 100644
index 0fefb8898..000000000
--- a/logging/example_logging.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import logging
-
-logging.basicConfig(level=logging.INFO)
-log = logging.getLogger("test")
-log.debug("Test message: %d(%s)", 100, "foobar")
-log.info("Test message2: %d(%s)", 100, "foobar")
diff --git a/logging/logging.py b/logging/logging.py
deleted file mode 100644
index 1c3ef0d84..000000000
--- a/logging/logging.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import sys
-
-CRITICAL = 50
-ERROR    = 40
-WARNING  = 30
-INFO     = 20
-DEBUG    = 10
-NOTSET   = 0
-
-_level_dict = {
-    CRITICAL: "CRIT",
-    ERROR: "ERROR",
-    WARNING: "WARN",
-    INFO: "INFO",
-    DEBUG: "DEBUG",
-}
-
-_stream = sys.stderr
-
-class Logger:
-
-    def __init__(self, name):
-        self.level = NOTSET
-        self.name = name
-
-    def _level_str(self, level):
-        if level in _level_dict:
-            return _level_dict[level]
-        return "LVL" + str(level)
-
-    def log(self, level, msg, *args):
-        if level >= (self.level or _level):
-            print(("%s:%s:" + msg) % ((self._level_str(level), self.name) + args), file=_stream)
-
-    def debug(self, msg, *args):
-        self.log(DEBUG, msg, *args)
-
-    def info(self, msg, *args):
-        self.log(INFO, msg, *args)
-
-    def warning(self, msg, *args):
-        self.log(WARNING, msg, *args)
-
-    def error(self, msg, *args):
-        self.log(ERROR, msg, *args)
-
-    def critical(self, msg, *args):
-        self.log(CRITICAL, msg, *args)
-
-
-_level = INFO
-_loggers = {}
-
-def getLogger(name):
-    if name in _loggers:
-        return _loggers[name]
-    l = Logger(name)
-    _loggers[name] = l
-    return l
-
-def info(msg, *args):
-    getLogger(None).info(msg, *args)
-
-def debug(msg, *args):
-    getLogger(None).debug(msg, *args)
-
-def basicConfig(level=INFO, filename=None, stream=None, format=None):
-    global _level, _stream
-    _level = level
-    if stream:
-        _stream = stream
-    if filename is not None:
-        print("logging.basicConfig: filename arg is not supported")
-    if format is not None:
-        print("logging.basicConfig: format arg is not supported")
diff --git a/logging/metadata.txt b/logging/metadata.txt
deleted file mode 100644
index 2254c557a..000000000
--- a/logging/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1.2
diff --git a/logging/setup.py b/logging/setup.py
deleted file mode 100644
index 9bc3ae4d1..000000000
--- a/logging/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-logging',
-      version='0.1.2',
-      description='logging module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['logging'])
diff --git a/machine/machine/__init__.py b/machine/machine/__init__.py
deleted file mode 100644
index b708c2e47..000000000
--- a/machine/machine/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .timer import *
diff --git a/mailbox/mailbox.py b/mailbox/mailbox.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mailbox/metadata.txt b/mailbox/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/mailbox/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/mailbox/setup.py b/mailbox/setup.py
deleted file mode 100644
index 5ab3a9fb1..000000000
--- a/mailbox/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-mailbox',
-      version='0.0.0',
-      description='Dummy mailbox module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['mailbox'])
diff --git a/mailcap/mailcap.py b/mailcap/mailcap.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mailcap/metadata.txt b/mailcap/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/mailcap/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/mailcap/setup.py b/mailcap/setup.py
deleted file mode 100644
index b20f9efb9..000000000
--- a/mailcap/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-mailcap',
-      version='0.0.0',
-      description='Dummy mailcap module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['mailcap'])
diff --git a/make_metadata.py b/make_metadata.py
deleted file mode 100755
index 836fb80e8..000000000
--- a/make_metadata.py
+++ /dev/null
@@ -1,172 +0,0 @@
-#!/usr/bin/env python3
-# MicroPython will pick up glob from the current dir otherwise.
-import sys
-sys.path.pop(0)
-
-import glob
-
-
-TEMPLATE = """\
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-%(dist_name)s',
-      version='%(version)s',
-      description=%(desc)r,
-      long_description=%(long_desc)r,
-      url='https://github.com/micropython/micropython/issues/405',
-      author=%(author)r,
-      author_email=%(author_email)r,
-      maintainer=%(maintainer)r,
-      maintainer_email='micro-python@googlegroups.com',
-      license=%(license)r,
-      %(_what_)s=[%(modules)s]%(_inst_req_)s)
-"""
-
-DUMMY_DESC = """\
-This is a dummy implementation of a module for MicroPython standard library.
-It contains zero or very little functionality, and primarily intended to
-avoid import errors (using idea that even if an application imports a
-module, it may be not using it onevery code path, so may work at least
-partially). It is expected that more complete implementation of the module
-will be provided later. Please help with the development if you are
-interested in this module."""
-
-CPYTHON_DESC = """\
-This is a module ported from CPython standard library to be compatible with
-MicroPython interpreter. Usually, this means applying small patches for
-features not supported (yet, or at all) in MicroPython. Sometimes, heavier
-changes are required. Note that CPython modules are written with availability
-of vast resources in mind, and may not work for MicroPython ports with
-limited heap. If you are affected by such a case, please help reimplement
-the module from scratch."""
-
-PYPY_DESC = """\
-This is a module ported from PyPy standard library to be compatible with
-MicroPython interpreter. Usually, this means applying small patches for
-features not supported (yet, or at all) in MicroPython. Sometimes, heavier
-changes are required. Note that CPython modules are written with availability
-of vast resources in mind, and may not work for MicroPython ports with
-limited heap. If you are affected by such a case, please help reimplement
-the module from scratch."""
-
-MICROPYTHON_LIB_DESC = """\
-This is a module reimplemented specifically for MicroPython standard library,
-with efficient and lean design in mind. Note that this module is likely work
-in progress and likely supports just a subset of CPython's corresponding
-module. Please help with the development if you are interested in this
-module."""
-
-BACKPORT_DESC = """\
-This is MicroPython compatibility module, allowing applications using
-MicroPython-specific features to run on CPython.
-"""
-
-MICROPYTHON_DEVELS = 'MicroPython Developers'
-MICROPYTHON_DEVELS_EMAIL = 'micro-python@googlegroups.com'
-CPYTHON_DEVELS = 'CPython Developers'
-CPYTHON_DEVELS_EMAIL = 'python-dev@python.org'
-PYPY_DEVELS = 'PyPy Developers'
-PYPY_DEVELS_EMAIL = 'pypy-dev@python.org'
-
-def parse_metadata(f):
-    data = {}
-    for l in f:
-        l = l.strip()
-        if l[0] == "#":
-            continue
-        k, v = l.split("=", 1)
-        data[k.strip()] = v.strip()
-    return data
-
-
-def write_setup(fname, substs):
-    with open(fname, "w") as f:
-        f.write(TEMPLATE % substs)
-
-
-def main():
-    for fname in glob.iglob("*/metadata.txt"):
-        print(fname)
-        with open(fname) as f:
-            data = parse_metadata(f)
-
-        dirname = fname.split("/")[0]
-        module = dirname
-        if data["type"] == "module":
-            data["_what_"] = "py_modules"
-        elif data["type"] == "package":
-            data["_what_"] = "packages"
-        else:
-            raise ValueError
-
-        if data["srctype"] == "dummy":
-            data["author"] = MICROPYTHON_DEVELS
-            data["author_email"] = MICROPYTHON_DEVELS_EMAIL
-            data["maintainer"] = MICROPYTHON_DEVELS
-            data["license"] = "MIT"
-            data["desc"] = "Dummy %s module for MicroPython" % module
-            data["long_desc"] = DUMMY_DESC
-        elif data["srctype"] == "cpython":
-            data["author"] = CPYTHON_DEVELS
-            data["author_email"] = CPYTHON_DEVELS_EMAIL
-            data["maintainer"] = MICROPYTHON_DEVELS
-            data["license"] = "Python"
-            data["desc"] = "CPython %s module ported to MicroPython" % module
-            data["long_desc"] = CPYTHON_DESC
-        elif data["srctype"] == "pypy":
-            data["author"] = PYPY_DEVELS
-            data["author_email"] = PYPY_DEVELS_EMAIL
-            data["maintainer"] = MICROPYTHON_DEVELS
-            data["license"] = "MIT"
-            data["desc"] = "PyPy %s module ported to MicroPython" % module
-            data["long_desc"] = PYPY_DESC
-        elif data["srctype"] == "micropython-lib":
-            if "author" not in data:
-                data["author"] = MICROPYTHON_DEVELS
-            if "author_email" not in data:
-                data["author_email"] = MICROPYTHON_DEVELS_EMAIL
-            if "maintainer" not in data:
-                data["maintainer"] = MICROPYTHON_DEVELS
-            if "desc" not in data:
-                data["desc"] = "%s module for MicroPython" % module
-            if "long_desc" not in data:
-                data["long_desc"] = MICROPYTHON_LIB_DESC
-            if "license" not in data:
-                data["license"] = "MIT"
-        elif data["srctype"] == "cpython-backport":
-            assert module.startswith("cpython-")
-            module = module[len("cpython-"):]
-            data["author"] = MICROPYTHON_DEVELS
-            data["author_email"] = MICROPYTHON_DEVELS_EMAIL
-            data["maintainer"] = MICROPYTHON_DEVELS
-            data["license"] = "Python"
-            data["desc"] = "MicroPython module %s ported to CPython" % module
-            data["long_desc"] = BACKPORT_DESC
-        else:
-            raise ValueError
-
-        if "dist_name" not in data:
-            data["dist_name"] = dirname
-        if "name" not in data:
-            data["name"] = module
-
-        data["modules"] = "'" + data["name"].split(".", 1)[0] + "'"
-        if "extra_modules" in data:
-            data["modules"] += ", " + ", ".join(["'" + x.strip() + "'" for x in data["extra_modules"].split(",")])
-
-        if "depends" in data:
-            deps = ["micropython-" + x.strip() for x in data["depends"].split(",")]
-            data["_inst_req_"] = ",\n      install_requires=['" + "', '".join(deps) + "']"
-        else:
-            data["_inst_req_"] = ""
-
-        write_setup(dirname + "/setup.py", data)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/micropython/README.md b/micropython/README.md
new file mode 100644
index 000000000..488bd50a0
--- /dev/null
+++ b/micropython/README.md
@@ -0,0 +1,9 @@
+## MicroPython-specific packages
+
+These are packages that have been written specifically for use on MicroPython.
+
+Packages in this directory should not have the same name as modules from the Python Standard Library.
+
+### Future plans
+
+* More organised directory structure based on purpose (e.g. drivers, network, etc).
diff --git a/micropython/aioespnow/README.md b/micropython/aioespnow/README.md
new file mode 100644
index 000000000..9774d19c3
--- /dev/null
+++ b/micropython/aioespnow/README.md
@@ -0,0 +1,91 @@
+# `aioespnow`
+
+A supplementary module which extends the micropython `espnow` module to provide
+`asyncio` support.
+
+- Asyncio support is available on all ESP32 targets as well as those ESP8266
+boards which include the `asyncio` module (ie. ESP8266 devices with at least
+2MB flash storage).
+
+## API reference
+
+- class `AIOESPNow()`: inherits all the methods of the `ESPNow` class and
+  extends the interface with the following async methods:
+
+  - `async AIOESPNow.arecv()`
+
+      Asyncio support for ESPNow.recv(). Note that this method does not take a
+      timeout value as argument.
+
+  - `async AIOESPNow.airecv()`
+
+      Asyncio support for ESPNow.irecv(). Use this method to reduce memory
+      fragmentation, as it will reuse common storage for each new message
+      received, whereas the `arecv()` method will allocate new memory for every
+      message received.
+
+  - `async AIOESPNow.asend(mac, msg, sync=True)`
+  - `async AIOESPNow.asend(msg)`
+
+      Asyncio support for ESPNow.send().
+
+  - `__aiter__()/async __anext__()`
+
+      AIOESPNow also supports reading incoming messages by asynchronous
+      iteration using `async for`, eg:
+
+    ```python
+      e = AIOESPNow()
+      e.active(True)
+      async def recv_till_halt(e):
+          async for mac, msg in e:
+              print(mac, msg)
+              if msg == b'halt':
+                  break
+      asyncio.run(recv_till_halt(e))
+    ```
+
+## Example Usage
+
+A small async server example::
+
+```python
+    import network
+    import aioespnow
+    import asyncio
+
+    # A WLAN interface must be active to send()/recv()
+    network.WLAN(network.WLAN.IF_STA).active(True)
+
+    e = aioespnow.AIOESPNow()  # Returns AIOESPNow enhanced with async support
+    e.active(True)
+    peer = b'\xbb\xbb\xbb\xbb\xbb\xbb'
+    e.add_peer(peer)
+
+    # Send a periodic ping to a peer
+    async def heartbeat(e, peer, period=30):
+        while True:
+            if not await e.asend(peer, b'ping'):
+                print("Heartbeat: peer not responding:", peer)
+            else:
+                print("Heartbeat: ping", peer)
+            await asyncio.sleep(period)
+
+    # Echo any received messages back to the sender
+    async def echo_server(e):
+        async for mac, msg in e:
+            print("Echo:", msg)
+            try:
+                await e.asend(mac, msg)
+            except OSError as err:
+                if len(err.args) > 1 and err.args[1] == 'ESP_ERR_ESPNOW_NOT_FOUND':
+                    e.add_peer(mac)
+                    await e.asend(mac, msg)
+
+    async def main(e, peer, timeout, period):
+        asyncio.create_task(heartbeat(e, peer, period))
+        asyncio.create_task(echo_server(e))
+        await asyncio.sleep(timeout)
+
+    asyncio.run(main(e, peer, 120, 10))
+```
diff --git a/micropython/aioespnow/aioespnow.py b/micropython/aioespnow/aioespnow.py
new file mode 100644
index 000000000..dec925de2
--- /dev/null
+++ b/micropython/aioespnow/aioespnow.py
@@ -0,0 +1,31 @@
+# aioespnow module for MicroPython on ESP32 and ESP8266
+# MIT license; Copyright (c) 2022 Glenn Moloney @glenn20
+
+import asyncio
+import espnow
+
+
+# Modelled on the asyncio.Stream class (extmod/asyncio/stream.py)
+# NOTE: Relies on internal implementation of asyncio.core (_io_queue)
+class AIOESPNow(espnow.ESPNow):
+    # Read one ESPNow message
+    async def arecv(self):
+        yield asyncio.core._io_queue.queue_read(self)
+        return self.recv(0)  # type: ignore[misc]
+
+    async def airecv(self):
+        yield asyncio.core._io_queue.queue_read(self)
+        return self.irecv(0)  # type: ignore[misc]
+
+    async def asend(self, mac, msg=None, sync=None):
+        if msg is None:
+            msg, mac = mac, None  # If msg is None: swap mac and msg
+        yield asyncio.core._io_queue.queue_write(self)
+        return self.send(mac, msg, sync)  # type: ignore[misc]
+
+    # "async for" support
+    def __aiter__(self):
+        return self
+
+    async def __anext__(self):
+        return await self.airecv()
diff --git a/micropython/aioespnow/manifest.py b/micropython/aioespnow/manifest.py
new file mode 100644
index 000000000..a91e48da6
--- /dev/null
+++ b/micropython/aioespnow/manifest.py
@@ -0,0 +1,6 @@
+metadata(
+    description="Extends the micropython espnow module with methods to support asyncio.",
+    version="0.1.0",
+)
+
+module("aioespnow.py")
diff --git a/micropython/aiorepl/README.md b/micropython/aiorepl/README.md
new file mode 100644
index 000000000..c1c08b899
--- /dev/null
+++ b/micropython/aiorepl/README.md
@@ -0,0 +1,101 @@
+# aiorepl
+
+This library provides "asyncio REPL", a simple REPL that can be used even
+while your program is running, allowing you to inspect program state, create
+tasks, and await asynchronous functions.
+
+This is inspired by Python's `asyncio` module when run via `python -m asyncio`.
+
+## Background
+
+The MicroPython REPL is unavailable while your program is running. This
+library runs a background REPL using the asyncio scheduler.
+
+Furthermore, it is not possible to `await` at the main REPL because it does
+not know about the asyncio scheduler.
+
+## Usage
+
+To use this library, you need to import the library and then start the REPL task.
+
+For example, in main.py:
+
+```py
+import asyncio
+import aiorepl
+
+async def demo():
+    await asyncio.sleep_ms(1000)
+    print("async demo")
+
+state = 20
+
+async def task1():
+    while state:
+        #print("task 1")
+        await asyncio.sleep_ms(500)
+    print("done")
+
+async def main():
+    print("Starting tasks...")
+
+    # Start other program tasks.
+    t1 = asyncio.create_task(task1())
+
+    # Start the aiorepl task.
+    repl = asyncio.create_task(aiorepl.task())
+
+    await asyncio.gather(t1, repl)
+
+asyncio.run(main())
+```
+
+An optional globals dictionary can be passed to `aiorepl.task()`, which allows
+you to specify what will be in scope for the REPL. By default it uses the
+globals dictionary from the `__main__` module, which is the same scope as the
+regular REPL (and `main.py`). In the example above, the REPL will be able to
+call the `demo()` function as well as get/set the `state` variable.
+
+You can also provide your own dictionary, e.g. `aiorepl.task({"obj": obj })`,
+or use the globals dict from the current module, e.g.
+`aiorepl.task(globals())`. Note that you cannot use a class instance's members
+dictionary, e.g. `aiorepl.task(obj.__dict__)`, as this is read-only in
+MicroPython.
+
+Instead of the regular `>>> ` prompt, the asyncio REPL will show `--> `.
+
+```
+--> 1+1
+2
+--> await demo()
+async demo
+--> state
+20
+--> import myapp.core
+--> state = await myapp.core.query_state()
+--> 1/0
+ZeroDivisionError: divide by zero
+--> def foo(x): return x + 1
+--> await asyncio.sleep(foo(3))
+--> 
+```
+
+History is supported via the up/down arrow keys.
+
+## Cancellation
+
+During command editing (the "R" phase), pressing Ctrl-C will cancel the current command and display a new prompt, like the regular REPL.
+
+While a command is being executed, Ctrl-C will cancel the task that is executing the command. This will have no effect on blocking code (e.g. `time.sleep()`), but this should be rare in an asyncio-based program.
+
+Ctrl-D at the asyncio REPL command prompt will terminate the current event loop, which will stop the running program and return to the regular REPL.
+
+## Limitations
+
+The following features are unsupported:
+
+* Tab completion is not supported (also unsupported in `python -m asyncio`).
+* Multi-line continuation. However you can do single-line definitions of functions, see demo above.
+* Exception tracebacks. Only the exception type and message is shown, see demo above.
+* Emacs shortcuts (e.g. Ctrl-A, Ctrl-E, to move to start/end of line).
+* Unicode handling for input.
diff --git a/micropython/aiorepl/aiorepl.py b/micropython/aiorepl/aiorepl.py
new file mode 100644
index 000000000..fbe513b7c
--- /dev/null
+++ b/micropython/aiorepl/aiorepl.py
@@ -0,0 +1,332 @@
+# MIT license; Copyright (c) 2022 Jim Mussared
+
+import micropython
+from micropython import const
+import re
+import sys
+import time
+import asyncio
+
+# Import statement (needs to be global, and does not return).
+_RE_IMPORT = re.compile("^import ([^ ]+)( as ([^ ]+))?")
+_RE_FROM_IMPORT = re.compile("^from [^ ]+ import ([^ ]+)( as ([^ ]+))?")
+# Global variable assignment.
+_RE_GLOBAL = re.compile("^([a-zA-Z0-9_]+) ?=[^=]")
+# General assignment expression or import statement (does not return a value).
+_RE_ASSIGN = re.compile("[^=]=[^=]")
+
+# Command hist (One reserved slot for the current command).
+_HISTORY_LIMIT = const(5 + 1)
+
+
+CHAR_CTRL_A = const(1)
+CHAR_CTRL_B = const(2)
+CHAR_CTRL_C = const(3)
+CHAR_CTRL_D = const(4)
+CHAR_CTRL_E = const(5)
+
+
+async def execute(code, g, s):
+    if not code.strip():
+        return
+
+    try:
+        if "await " in code:
+            # Execute the code snippet in an async context.
+            if m := _RE_IMPORT.match(code) or _RE_FROM_IMPORT.match(code):
+                code = "global {}\n    {}".format(m.group(3) or m.group(1), code)
+            elif m := _RE_GLOBAL.match(code):
+                code = "global {}\n    {}".format(m.group(1), code)
+            elif not _RE_ASSIGN.search(code):
+                code = "return {}".format(code)
+
+            code = """
+import asyncio
+async def __code():
+    {}
+
+__exec_task = asyncio.create_task(__code())
+""".format(code)
+
+            async def kbd_intr_task(exec_task, s):
+                while True:
+                    if ord(await s.read(1)) == CHAR_CTRL_C:
+                        exec_task.cancel()
+                        return
+
+            l = {"__exec_task": None}
+            exec(code, g, l)
+            exec_task = l["__exec_task"]
+
+            # Concurrently wait for either Ctrl-C from the stream or task
+            # completion.
+            intr_task = asyncio.create_task(kbd_intr_task(exec_task, s))
+
+            try:
+                try:
+                    return await exec_task
+                except asyncio.CancelledError:
+                    pass
+            finally:
+                intr_task.cancel()
+                try:
+                    await intr_task
+                except asyncio.CancelledError:
+                    pass
+        else:
+            # Excute code snippet directly.
+            try:
+                try:
+                    micropython.kbd_intr(3)
+                    try:
+                        return eval(code, g)
+                    except SyntaxError:
+                        # Maybe an assignment, try with exec.
+                        return exec(code, g)
+                except KeyboardInterrupt:
+                    pass
+            finally:
+                micropython.kbd_intr(-1)
+
+    except Exception as err:
+        print("{}: {}".format(type(err).__name__, err))
+
+
+# REPL task. Invoke this with an optional mutable globals dict.
+async def task(g=None, prompt="--> "):
+    print("Starting asyncio REPL...")
+    if g is None:
+        g = __import__("__main__").__dict__
+    try:
+        micropython.kbd_intr(-1)
+        s = asyncio.StreamReader(sys.stdin)
+        # clear = True
+        hist = [None] * _HISTORY_LIMIT
+        hist_i = 0  # Index of most recent entry.
+        hist_n = 0  # Number of history entries.
+        c = 0  # ord of most recent character.
+        t = 0  # timestamp of most recent character.
+        while True:
+            hist_b = 0  # How far back in the history are we currently.
+            sys.stdout.write(prompt)
+            cmd: str = ""
+            paste = False
+            curs = 0  # cursor offset from end of cmd buffer
+            while True:
+                b = await s.read(1)
+                if not b:  # Handle EOF/empty read
+                    break
+                pc = c  # save previous character
+                c = ord(b)
+                pt = t  # save previous time
+                t = time.ticks_ms()
+                if c < 0x20 or c > 0x7E:
+                    if c == 0x0A:
+                        # LF
+                        if paste:
+                            sys.stdout.write(b)
+                            cmd += b
+                            continue
+                        # If the previous character was also LF, and was less
+                        # than 20 ms ago, this was likely due to CRLF->LFLF
+                        # conversion, so ignore this linefeed.
+                        if pc == 0x0A and time.ticks_diff(t, pt) < 20:
+                            continue
+                        if curs:
+                            # move cursor to end of the line
+                            sys.stdout.write("\x1b[{}C".format(curs))
+                            curs = 0
+                        sys.stdout.write("\n")
+                        if cmd:
+                            # Push current command.
+                            hist[hist_i] = cmd
+                            # Increase history length if possible, and rotate ring forward.
+                            hist_n = min(_HISTORY_LIMIT - 1, hist_n + 1)
+                            hist_i = (hist_i + 1) % _HISTORY_LIMIT
+
+                            result = await execute(cmd, g, s)
+                            if result is not None:
+                                sys.stdout.write(repr(result))
+                                sys.stdout.write("\n")
+                        break
+                    elif c == 0x08 or c == 0x7F:
+                        # Backspace.
+                        if cmd:
+                            if curs:
+                                cmd = "".join((cmd[: -curs - 1], cmd[-curs:]))
+                                sys.stdout.write(
+                                    "\x08\x1b[K"
+                                )  # move cursor back, erase to end of line
+                                sys.stdout.write(cmd[-curs:])  # redraw line
+                                sys.stdout.write("\x1b[{}D".format(curs))  # reset cursor location
+                            else:
+                                cmd = cmd[:-1]
+                                sys.stdout.write("\x08 \x08")
+                    elif c == CHAR_CTRL_A:
+                        raw_repl(sys.stdin, g)
+                        break
+                    elif c == CHAR_CTRL_B:
+                        continue
+                    elif c == CHAR_CTRL_C:
+                        if paste:
+                            break
+                        sys.stdout.write("\n")
+                        break
+                    elif c == CHAR_CTRL_D:
+                        if paste:
+                            result = await execute(cmd, g, s)
+                            if result is not None:
+                                sys.stdout.write(repr(result))
+                                sys.stdout.write("\n")
+                            break
+
+                        sys.stdout.write("\n")
+                        # Shutdown asyncio.
+                        asyncio.new_event_loop()
+                        return
+                    elif c == CHAR_CTRL_E:
+                        sys.stdout.write("paste mode; Ctrl-C to cancel, Ctrl-D to finish\n===\n")
+                        paste = True
+                    elif c == 0x1B:
+                        # Start of escape sequence.
+                        key = await s.read(2)
+                        if key in ("[A", "[B"):  # up, down
+                            # Stash the current command.
+                            hist[(hist_i - hist_b) % _HISTORY_LIMIT] = cmd
+                            # Clear current command.
+                            b = "\x08" * len(cmd)
+                            sys.stdout.write(b)
+                            sys.stdout.write(" " * len(cmd))
+                            sys.stdout.write(b)
+                            # Go backwards or forwards in the history.
+                            if key == "[A":
+                                hist_b = min(hist_n, hist_b + 1)
+                            else:
+                                hist_b = max(0, hist_b - 1)
+                            # Update current command.
+                            cmd = hist[(hist_i - hist_b) % _HISTORY_LIMIT]
+                            sys.stdout.write(cmd)
+                        elif key == "[D":  # left
+                            if curs < len(cmd) - 1:
+                                curs += 1
+                                sys.stdout.write("\x1b")
+                                sys.stdout.write(key)
+                        elif key == "[C":  # right
+                            if curs:
+                                curs -= 1
+                                sys.stdout.write("\x1b")
+                                sys.stdout.write(key)
+                        elif key == "[H":  # home
+                            pcurs = curs
+                            curs = len(cmd)
+                            sys.stdout.write("\x1b[{}D".format(curs - pcurs))  # move cursor left
+                        elif key == "[F":  # end
+                            pcurs = curs
+                            curs = 0
+                            sys.stdout.write("\x1b[{}C".format(pcurs))  # move cursor right
+                    else:
+                        # sys.stdout.write("\\x")
+                        # sys.stdout.write(hex(c))
+                        pass
+                else:
+                    if curs:
+                        # inserting into middle of line
+                        cmd = "".join((cmd[:-curs], b, cmd[-curs:]))
+                        sys.stdout.write(cmd[-curs - 1 :])  # redraw line to end
+                        sys.stdout.write("\x1b[{}D".format(curs))  # reset cursor location
+                    else:
+                        sys.stdout.write(b)
+                        cmd += b
+    finally:
+        micropython.kbd_intr(3)
+
+
+def raw_paste(s, window=512):
+    sys.stdout.write("R\x01")  # supported
+    sys.stdout.write(bytearray([window & 0xFF, window >> 8, 0x01]).decode())
+    eof = False
+    idx = 0
+    buff = bytearray(window)
+    file = b""
+    while not eof:
+        for idx in range(window):
+            b = s.read(1)
+            c = ord(b)
+            if c == CHAR_CTRL_C or c == CHAR_CTRL_D:
+                # end of file
+                sys.stdout.write(chr(CHAR_CTRL_D))
+                if c == CHAR_CTRL_C:
+                    raise KeyboardInterrupt
+                file += buff[:idx]
+                eof = True
+                break
+            buff[idx] = c
+
+        if not eof:
+            file += buff
+            sys.stdout.write("\x01")  # indicate window available to host
+
+    return file
+
+
+def raw_repl(s, g: dict):
+    """
+    This function is blocking to prevent other
+    async tasks from writing to the stdio stream and
+    breaking the raw repl session.
+    """
+    heading = "raw REPL; CTRL-B to exit\n"
+    line = ""
+    sys.stdout.write(heading)
+
+    while True:
+        line = ""
+        sys.stdout.write(">")
+        while True:
+            b = s.read(1)
+            c = ord(b)
+            if c == CHAR_CTRL_A:
+                rline = line
+                line = ""
+
+                if len(rline) == 2 and ord(rline[0]) == CHAR_CTRL_E:
+                    if rline[1] == "A":
+                        line = raw_paste(s)
+                        break
+                else:
+                    # reset raw REPL
+                    sys.stdout.write(heading)
+                    sys.stdout.write(">")
+                continue
+            elif c == CHAR_CTRL_B:
+                # exit raw REPL
+                sys.stdout.write("\n")
+                return 0
+            elif c == CHAR_CTRL_C:
+                # clear line
+                line = ""
+            elif c == CHAR_CTRL_D:
+                # entry finished
+                # indicate reception of command
+                sys.stdout.write("OK")
+                break
+            else:
+                # let through any other raw 8-bit value
+                line += b
+
+        if len(line) == 0:
+            # Normally used to trigger soft-reset but stay in raw mode.
+            # Fake it for aiorepl / mpremote.
+            sys.stdout.write("Ignored: soft reboot\n")
+            sys.stdout.write(heading)
+
+        try:
+            result = exec(line, g)
+            if result is not None:
+                sys.stdout.write(repr(result))
+            sys.stdout.write(chr(CHAR_CTRL_D))
+        except Exception as ex:
+            print(line)
+            sys.stdout.write(chr(CHAR_CTRL_D))
+            sys.print_exception(ex, sys.stdout)
+        sys.stdout.write(chr(CHAR_CTRL_D))
diff --git a/micropython/aiorepl/manifest.py b/micropython/aiorepl/manifest.py
new file mode 100644
index 000000000..83802e1c0
--- /dev/null
+++ b/micropython/aiorepl/manifest.py
@@ -0,0 +1,6 @@
+metadata(
+    version="0.2.2",
+    description="Provides an asynchronous REPL that can run concurrently with an asyncio, also allowing await expressions.",
+)
+
+module("aiorepl.py")
diff --git a/micropython/bluetooth/aioble-central/manifest.py b/micropython/bluetooth/aioble-central/manifest.py
new file mode 100644
index 000000000..ed61ec9d7
--- /dev/null
+++ b/micropython/bluetooth/aioble-central/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.3.0")
+
+require("aioble-core")
+
+package("aioble", files=("central.py",), base_path="../aioble")
diff --git a/micropython/bluetooth/aioble-client/manifest.py b/micropython/bluetooth/aioble-client/manifest.py
new file mode 100644
index 000000000..163cbe23d
--- /dev/null
+++ b/micropython/bluetooth/aioble-client/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.3.0")
+
+require("aioble-core")
+
+package("aioble", files=("client.py",), base_path="../aioble")
diff --git a/micropython/bluetooth/aioble-core/manifest.py b/micropython/bluetooth/aioble-core/manifest.py
new file mode 100644
index 000000000..e040f1076
--- /dev/null
+++ b/micropython/bluetooth/aioble-core/manifest.py
@@ -0,0 +1,11 @@
+metadata(version="0.4.0")
+
+package(
+    "aioble",
+    files=(
+        "__init__.py",
+        "core.py",
+        "device.py",
+    ),
+    base_path="../aioble",
+)
diff --git a/micropython/bluetooth/aioble-l2cap/manifest.py b/micropython/bluetooth/aioble-l2cap/manifest.py
new file mode 100644
index 000000000..34a96344e
--- /dev/null
+++ b/micropython/bluetooth/aioble-l2cap/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.2.1")
+
+require("aioble-core")
+
+package("aioble", files=("l2cap.py",), base_path="../aioble")
diff --git a/micropython/bluetooth/aioble-peripheral/manifest.py b/micropython/bluetooth/aioble-peripheral/manifest.py
new file mode 100644
index 000000000..0aec4d21e
--- /dev/null
+++ b/micropython/bluetooth/aioble-peripheral/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.2.1")
+
+require("aioble-core")
+
+package("aioble", files=("peripheral.py",), base_path="../aioble")
diff --git a/micropython/bluetooth/aioble-security/manifest.py b/micropython/bluetooth/aioble-security/manifest.py
new file mode 100644
index 000000000..5737d2a06
--- /dev/null
+++ b/micropython/bluetooth/aioble-security/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.2.0")
+
+require("aioble-core")
+
+package("aioble", files=("security.py",), base_path="../aioble")
diff --git a/micropython/bluetooth/aioble-server/manifest.py b/micropython/bluetooth/aioble-server/manifest.py
new file mode 100644
index 000000000..c5b12ffbd
--- /dev/null
+++ b/micropython/bluetooth/aioble-server/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.4.1")
+
+require("aioble-core")
+
+package("aioble", files=("server.py",), base_path="../aioble")
diff --git a/micropython/bluetooth/aioble/README.md b/micropython/bluetooth/aioble/README.md
new file mode 100644
index 000000000..83ae00209
--- /dev/null
+++ b/micropython/bluetooth/aioble/README.md
@@ -0,0 +1,249 @@
+aioble
+======
+
+This library provides an object-oriented, asyncio-based wrapper for MicroPython's
+[bluetooth](https://docs.micropython.org/en/latest/library/bluetooth.html) API.
+
+**Note**: aioble requires MicroPython v1.17 or higher.
+
+Features
+--------
+
+Broadcaster (advertiser) role:
+* Generate advertising and scan response payloads for common fields.
+* Automatically split payload over advertising and scan response.
+* Start advertising (indefinitely or for duration).
+
+Peripheral role:
+* Wait for connection from central.
+* Wait for MTU exchange.
+
+Observer (scanner) role:
+* Scan for devices (passive + active).
+* Combine advertising and scan response payloads for the same device.
+* Parse common fields from advertising payloads.
+
+Central role:
+* Connect to peripheral.
+* Initiate MTU exchange.
+
+GATT Client:
+* Discover services, characteristics, and descriptors (optionally by UUID).
+* Read / write / write-with-response characters and descriptors.
+* Subscribe to notifications and indications on characteristics (via the CCCD).
+* Wait for notifications and indications.
+
+GATT Server:
+* Register services, characteristics, and descriptors.
+* Wait for writes on characteristics and descriptors.
+* Intercept read requests.
+* Send notifications and indications (and wait on response).
+
+L2CAP:
+* Accept and connect L2CAP Connection-oriented-channels.
+* Manage channel flow control.
+
+Security:
+* JSON-backed key/secret management.
+* Initiate pairing.
+* Query encryption/authentication state.
+
+All remote operations (connect, disconnect, client read/write, server indicate, l2cap recv/send, pair) are awaitable and support timeouts.
+
+Installation
+------------
+
+You can install any combination of the following packages.
+- `aioble-central` -- Central (and Observer) role functionality including
+  scanning and connecting.
+- `aioble-client` -- GATT client, typically used by central role devices but
+  can also be used on peripherals.
+- `aioble-l2cap` -- L2CAP Connection-oriented-channels support.
+- `aioble-peripheral` -- Peripheral (and Broadcaster) role functionality
+  including advertising.
+- `aioble-security` -- Pairing and bonding support.
+- `aioble-server` -- GATT server, typically used by peripheral role devices
+  but can also be used on centrals.
+
+Alternatively, install the `aioble` package, which will install everything.
+
+Usage
+-----
+
+#### Passive scan for nearby devices for 5 seconds: (Observer)
+
+```py
+async with aioble.scan(duration_ms=5000) as scanner:
+    async for result in scanner:
+        print(result, result.name(), result.rssi, result.services())
+```
+
+Active scan (includes "scan response" data) for nearby devices for 5 seconds
+with the highest duty cycle: (Observer)
+
+```py
+async with aioble.scan(duration_ms=5000, interval_us=30000, window_us=30000, active=True) as scanner:
+    async for result in scanner:
+        print(result, result.name(), result.rssi, result.services())
+```
+
+#### Connect to a peripheral device: (Central)
+
+```py
+# Either from scan result
+device = result.device
+# Or with known address
+device = aioble.Device(aioble.PUBLIC, "aa:bb:cc:dd:ee:ff")
+
+try:
+    connection = await device.connect(timeout_ms=2000)
+except asyncio.TimeoutError:
+    print('Timeout')
+```
+
+#### Register services and wait for connection: (Peripheral, Server)
+
+```py
+_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
+_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)
+_GENERIC_THERMOMETER = const(768)
+
+_ADV_INTERVAL_US = const(250000)
+
+temp_service = aioble.Service(_ENV_SENSE_UUID)
+temp_char = aioble.Characteristic(temp_service, _ENV_SENSE_TEMP_UUID, read=True, notify=True)
+
+aioble.register_services(temp_service)
+
+while True:
+    connection = await aioble.advertise(
+            _ADV_INTERVAL_US,
+            name="temp-sense",
+            services=[_ENV_SENSE_UUID],
+            appearance=_GENERIC_THERMOMETER,
+            manufacturer=(0xabcd, b"1234"),
+        )
+    print("Connection from", device)
+```
+
+#### Update characteristic value: (Server)
+
+```py
+# Write the local value.
+temp_char.write(b'data')
+```
+
+```py
+# Write the local value and notify/indicate subscribers.
+temp_char.write(b'data', send_update=True)
+```
+
+#### Send notifications: (Server)
+
+```py
+# Notify with the current value.
+temp_char.notify(connection)
+```
+
+```py
+# Notify with a custom value.
+temp_char.notify(connection, b'optional data')
+```
+
+#### Send indications: (Server)
+
+```py
+# Indicate with current value.
+await temp_char.indicate(connection, timeout_ms=2000)
+```
+
+```py
+# Indicate with custom value.
+await temp_char.indicate(connection, b'optional data', timeout_ms=2000)
+```
+
+This will raise `GattError` if the indication is not acknowledged.
+
+#### Wait for a write from the client: (Server)
+
+```py
+# Normal characteristic, returns the connection that did the write.
+connection = await char.written(timeout_ms=2000)
+```
+
+```py
+# Characteristic with capture enabled, also returns the value.
+char = Characteristic(..., capture=True)
+connection, data = await char.written(timeout_ms=2000)
+```
+
+#### Query the value of a characteristic: (Client)
+
+```py
+temp_service = await connection.service(_ENV_SENSE_UUID)
+temp_char = await temp_service.characteristic(_ENV_SENSE_TEMP_UUID)
+
+data = await temp_char.read(timeout_ms=1000)
+```
+
+#### Wait for a notification/indication: (Client)
+
+```py
+# Notification
+data = await temp_char.notified(timeout_ms=1000)
+```
+
+```py
+# Indication
+data = await temp_char.indicated(timeout_ms=1000)
+```
+
+#### Subscribe to a characteristic: (Client)
+
+```py
+# Subscribe for notification.
+await temp_char.subscribe(notify=True)
+while True:
+    data = await temp_char.notified()
+```
+
+```py
+# Subscribe for indication.
+await temp_char.subscribe(indicate=True)
+while True:
+    data = await temp_char.indicated()
+```
+
+#### Open L2CAP channels: (Listener)
+
+```py
+channel = await connection.l2cap_accept(_L2CAP_PSN, _L2CAP_MTU)
+buf = bytearray(64)
+n = channel.recvinto(buf)
+channel.send(b'response')
+```
+
+#### Open L2CAP channels: (Initiator)
+
+```py
+channel = await connection.l2cap_connect(_L2CAP_PSN, _L2CAP_MTU)
+channel.send(b'request')
+buf = bytearray(64)
+n = channel.recvinto(buf)
+```
+
+
+Examples
+--------
+
+See the `examples` directory for some example applications.
+
+* temp_sensor.py: Temperature sensor peripheral.
+* temp_client.py: Connects to the temp sensor.
+* l2cap_file_server.py: Simple file server peripheral. (WIP)
+* l2cap_file_client.py: Client for the file server. (WIP)
+
+Tests
+-----
+
+The `multitests` directory provides tests that can be run with MicroPython's `run-multitests.py` script. These are based on the existing `multi_bluetooth` tests that are in the main repo.
diff --git a/micropython/bluetooth/aioble/aioble/__init__.py b/micropython/bluetooth/aioble/aioble/__init__.py
new file mode 100644
index 000000000..dde89f5e7
--- /dev/null
+++ b/micropython/bluetooth/aioble/aioble/__init__.py
@@ -0,0 +1,32 @@
+# MicroPython aioble module
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+from micropython import const
+
+from .device import Device, DeviceDisconnectedError
+from .core import log_info, log_warn, log_error, GattError, config, stop
+
+try:
+    from .peripheral import advertise
+except:
+    log_info("Peripheral support disabled")
+
+try:
+    from .central import scan
+except:
+    log_info("Central support disabled")
+
+try:
+    from .server import (
+        Service,
+        Characteristic,
+        BufferedCharacteristic,
+        Descriptor,
+        register_services,
+    )
+except:
+    log_info("GATT server support disabled")
+
+
+ADDR_PUBLIC = const(0)
+ADDR_RANDOM = const(1)
diff --git a/micropython/bluetooth/aioble/aioble/central.py b/micropython/bluetooth/aioble/aioble/central.py
new file mode 100644
index 000000000..131b1e0db
--- /dev/null
+++ b/micropython/bluetooth/aioble/aioble/central.py
@@ -0,0 +1,307 @@
+# MicroPython aioble module
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+from micropython import const
+
+import bluetooth
+import struct
+
+import asyncio
+
+from .core import (
+    ensure_active,
+    ble,
+    log_info,
+    log_error,
+    log_warn,
+    register_irq_handler,
+)
+from .device import Device, DeviceConnection, DeviceTimeout
+
+
+_IRQ_SCAN_RESULT = const(5)
+_IRQ_SCAN_DONE = const(6)
+
+_IRQ_PERIPHERAL_CONNECT = const(7)
+_IRQ_PERIPHERAL_DISCONNECT = const(8)
+
+_ADV_IND = const(0)
+_ADV_DIRECT_IND = const(1)
+_ADV_SCAN_IND = const(2)
+_ADV_NONCONN_IND = const(3)
+_SCAN_RSP = const(4)
+
+_ADV_TYPE_FLAGS = const(0x01)
+_ADV_TYPE_NAME = const(0x09)
+_ADV_TYPE_SHORT_NAME = const(0x08)
+_ADV_TYPE_UUID16_INCOMPLETE = const(0x2)
+_ADV_TYPE_UUID16_COMPLETE = const(0x3)
+_ADV_TYPE_UUID32_INCOMPLETE = const(0x4)
+_ADV_TYPE_UUID32_COMPLETE = const(0x5)
+_ADV_TYPE_UUID128_INCOMPLETE = const(0x6)
+_ADV_TYPE_UUID128_COMPLETE = const(0x7)
+_ADV_TYPE_APPEARANCE = const(0x19)
+_ADV_TYPE_MANUFACTURER = const(0xFF)
+
+
+# Keep track of the active scanner so IRQs can be delivered to it.
+_active_scanner = None
+
+
+# Set of devices that are waiting for the peripheral connect IRQ.
+_connecting = set()
+
+
+def _central_irq(event, data):
+    # Send results and done events to the active scanner instance.
+    if event == _IRQ_SCAN_RESULT:
+        addr_type, addr, adv_type, rssi, adv_data = data
+        if not _active_scanner:
+            return
+        _active_scanner._queue.append((addr_type, bytes(addr), adv_type, rssi, bytes(adv_data)))
+        _active_scanner._event.set()
+    elif event == _IRQ_SCAN_DONE:
+        if not _active_scanner:
+            return
+        _active_scanner._done = True
+        _active_scanner._event.set()
+
+    # Peripheral connect must be in response to a pending connection, so find
+    # it in the pending connection set.
+    elif event == _IRQ_PERIPHERAL_CONNECT:
+        conn_handle, addr_type, addr = data
+
+        for d in _connecting:
+            if d.addr_type == addr_type and d.addr == addr:
+                # Allow connect() to complete.
+                connection = d._connection
+                connection._conn_handle = conn_handle
+                connection._event.set()
+                break
+
+    # Find the active device connection for this connection handle.
+    elif event == _IRQ_PERIPHERAL_DISCONNECT:
+        conn_handle, _, _ = data
+        if connection := DeviceConnection._connected.get(conn_handle, None):
+            # Tell the device_task that it should terminate.
+            connection._event.set()
+
+
+def _central_shutdown():
+    global _active_scanner, _connecting
+    _active_scanner = None
+    _connecting = set()
+
+
+register_irq_handler(_central_irq, _central_shutdown)
+
+
+# Cancel an in-progress scan.
+async def _cancel_pending():
+    if _active_scanner:
+        await _active_scanner.cancel()
+
+
+# Start connecting to a peripheral.
+# Call device.connect() rather than using method directly.
+async def _connect(
+    connection, timeout_ms, scan_duration_ms, min_conn_interval_us, max_conn_interval_us
+):
+    device = connection.device
+    if device in _connecting:
+        return
+
+    # Enable BLE and cancel in-progress scans.
+    ensure_active()
+    await _cancel_pending()
+
+    # Allow the connected IRQ to find the device by address.
+    _connecting.add(device)
+
+    # Event will be set in the connected IRQ, and then later
+    # re-used to notify disconnection.
+    connection._event = connection._event or asyncio.ThreadSafeFlag()
+
+    try:
+        with DeviceTimeout(None, timeout_ms):
+            ble.gap_connect(
+                device.addr_type,
+                device.addr,
+                scan_duration_ms,
+                min_conn_interval_us,
+                max_conn_interval_us,
+            )
+
+            # Wait for the connected IRQ.
+            await connection._event.wait()
+            assert connection._conn_handle is not None
+
+            # Register connection handle -> device.
+            DeviceConnection._connected[connection._conn_handle] = connection
+    finally:
+        # After timeout, don't hold a reference and ignore future events.
+        _connecting.remove(device)
+
+
+# Represents a single device that has been found during a scan. The scan
+# iterator will return the same ScanResult instance multiple times as its data
+# changes (i.e. changing RSSI or advertising data).
+class ScanResult:
+    def __init__(self, device):
+        self.device = device
+        self.adv_data = None
+        self.resp_data = None
+        self.rssi = None
+        self.connectable = False
+
+    # New scan result available, return true if it changes our state.
+    def _update(self, adv_type, rssi, adv_data):
+        updated = False
+
+        if rssi != self.rssi:
+            self.rssi = rssi
+            updated = True
+
+        if adv_type in (_ADV_IND, _ADV_NONCONN_IND):
+            if adv_data != self.adv_data:
+                self.adv_data = adv_data
+                self.connectable = adv_type == _ADV_IND
+                updated = True
+        elif adv_type == _ADV_SCAN_IND:
+            if adv_data != self.adv_data and self.resp_data:
+                updated = True
+            self.adv_data = adv_data
+        elif adv_type == _SCAN_RSP and adv_data:
+            if adv_data != self.resp_data:
+                self.resp_data = adv_data
+                updated = True
+
+        return updated
+
+    def __str__(self):
+        return "Scan result: {} {}".format(self.device, self.rssi)
+
+    # Gets all the fields for the specified types.
+    def _decode_field(self, *adv_type):
+        # Advertising payloads are repeated packets of the following form:
+        #   1 byte data length (N + 1)
+        #   1 byte type (see constants below)
+        #   N bytes type-specific data
+        for payload in (self.adv_data, self.resp_data):
+            if not payload:
+                continue
+            i = 0
+            while i + 1 < len(payload):
+                if payload[i + 1] in adv_type:
+                    yield payload[i + 2 : i + payload[i] + 1]
+                i += 1 + payload[i]
+
+    # Returns the value of the complete (or shortened) advertised name, if available.
+    def name(self):
+        for n in self._decode_field(_ADV_TYPE_NAME, _ADV_TYPE_SHORT_NAME):
+            return str(n, "utf-8") if n else ""
+
+    # Generator that enumerates the service UUIDs that are advertised.
+    def services(self):
+        for uuid_len, codes in (
+            (2, (_ADV_TYPE_UUID16_INCOMPLETE, _ADV_TYPE_UUID16_COMPLETE)),
+            (4, (_ADV_TYPE_UUID32_INCOMPLETE, _ADV_TYPE_UUID32_COMPLETE)),
+            (16, (_ADV_TYPE_UUID128_INCOMPLETE, _ADV_TYPE_UUID128_COMPLETE)),
+        ):
+            for u in self._decode_field(*codes):
+                for i in range(0, len(u), uuid_len):
+                    yield bluetooth.UUID(u[i : i + uuid_len])
+
+    # Generator that returns (manufacturer_id, data) tuples.
+    def manufacturer(self, filter=None):
+        for u in self._decode_field(_ADV_TYPE_MANUFACTURER):
+            if len(u) < 2:
+                continue
+            m = struct.unpack("<H", u[0:2])[0]
+            if filter is None or m == filter:
+                yield (m, u[2:])
+
+
+# Use with:
+# async with aioble.scan(...) as scanner:
+#   async for result in scanner:
+#     ...
+class scan:
+    def __init__(self, duration_ms, interval_us=None, window_us=None, active=False):
+        self._queue = []
+        self._event = asyncio.ThreadSafeFlag()
+        self._done = False
+
+        # Keep track of what we've already seen.
+        self._results = set()
+
+        # Ideally we'd start the scan here and avoid having to save these
+        # values, but we need to stop any previous scan first via awaiting
+        # _cancel_pending(), but __init__ isn't async.
+        self._duration_ms = duration_ms
+        self._interval_us = interval_us or 1280000
+        self._window_us = window_us or 11250
+        self._active = active
+
+    async def __aenter__(self):
+        global _active_scanner
+        ensure_active()
+        await _cancel_pending()
+        _active_scanner = self
+        ble.gap_scan(self._duration_ms, self._interval_us, self._window_us, self._active)
+        return self
+
+    async def __aexit__(self, exc_type, exc_val, exc_traceback):
+        # Cancel the current scan if we're still the active scanner. This will
+        # happen if the loop breaks early before the scan duration completes.
+        if _active_scanner == self:
+            await self.cancel()
+
+    def __aiter__(self):
+        assert _active_scanner == self
+        return self
+
+    async def __anext__(self):
+        global _active_scanner
+
+        if _active_scanner != self:
+            # The scan has been canceled (e.g. a connection was initiated).
+            raise StopAsyncIteration
+
+        while True:
+            while self._queue:
+                addr_type, addr, adv_type, rssi, adv_data = self._queue.pop()
+
+                # Try to find an existing ScanResult for this device.
+                for r in self._results:
+                    if r.device.addr_type == addr_type and r.device.addr == addr:
+                        result = r
+                        break
+                else:
+                    # New device, create a new Device & ScanResult.
+                    device = Device(addr_type, addr)
+                    result = ScanResult(device)
+                    self._results.add(result)
+
+                # Add the new information from this event.
+                if result._update(adv_type, rssi, adv_data):
+                    # It's new information, so re-yield this result.
+                    return result
+
+            if self._done:
+                # _IRQ_SCAN_DONE event was fired.
+                _active_scanner = None
+                raise StopAsyncIteration
+
+            # Wait for either done or result IRQ.
+            await self._event.wait()
+
+    # Cancel any in-progress scan. We need to do this before starting any other operation.
+    async def cancel(self):
+        if self._done:
+            return
+        ble.gap_scan(None)
+        while not self._done:
+            await self._event.wait()
+        global _active_scanner
+        _active_scanner = None
diff --git a/micropython/bluetooth/aioble/aioble/client.py b/micropython/bluetooth/aioble/aioble/client.py
new file mode 100644
index 000000000..859c6e937
--- /dev/null
+++ b/micropython/bluetooth/aioble/aioble/client.py
@@ -0,0 +1,456 @@
+# MicroPython aioble module
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+from micropython import const
+from collections import deque
+import asyncio
+import struct
+
+import bluetooth
+
+from .core import ble, GattError, register_irq_handler
+from .device import DeviceConnection
+
+
+_IRQ_GATTC_SERVICE_RESULT = const(9)
+_IRQ_GATTC_SERVICE_DONE = const(10)
+_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
+_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
+_IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
+_IRQ_GATTC_DESCRIPTOR_DONE = const(14)
+_IRQ_GATTC_READ_RESULT = const(15)
+_IRQ_GATTC_READ_DONE = const(16)
+_IRQ_GATTC_WRITE_DONE = const(17)
+_IRQ_GATTC_NOTIFY = const(18)
+_IRQ_GATTC_INDICATE = const(19)
+
+_CCCD_UUID = const(0x2902)
+_CCCD_NOTIFY = const(1)
+_CCCD_INDICATE = const(2)
+
+_FLAG_READ = const(0x0002)
+_FLAG_WRITE_NO_RESPONSE = const(0x0004)
+_FLAG_WRITE = const(0x0008)
+_FLAG_NOTIFY = const(0x0010)
+_FLAG_INDICATE = const(0x0020)
+
+
+# Forward IRQs directly to static methods on the type that handles them and
+# knows how to map handles to instances. Note: We copy all uuid and data
+# params here for safety, but a future optimisation might be able to avoid
+# these copies in a few places.
+def _client_irq(event, data):
+    if event == _IRQ_GATTC_SERVICE_RESULT:
+        conn_handle, start_handle, end_handle, uuid = data
+        ClientDiscover._discover_result(
+            conn_handle, start_handle, end_handle, bluetooth.UUID(uuid)
+        )
+    elif event == _IRQ_GATTC_SERVICE_DONE:
+        conn_handle, status = data
+        ClientDiscover._discover_done(conn_handle, status)
+    elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
+        conn_handle, end_handle, value_handle, properties, uuid = data
+        ClientDiscover._discover_result(
+            conn_handle, end_handle, value_handle, properties, bluetooth.UUID(uuid)
+        )
+    elif event == _IRQ_GATTC_CHARACTERISTIC_DONE:
+        conn_handle, status = data
+        ClientDiscover._discover_done(conn_handle, status)
+    elif event == _IRQ_GATTC_DESCRIPTOR_RESULT:
+        conn_handle, dsc_handle, uuid = data
+        ClientDiscover._discover_result(conn_handle, dsc_handle, bluetooth.UUID(uuid))
+    elif event == _IRQ_GATTC_DESCRIPTOR_DONE:
+        conn_handle, status = data
+        ClientDiscover._discover_done(conn_handle, status)
+    elif event == _IRQ_GATTC_READ_RESULT:
+        conn_handle, value_handle, char_data = data
+        ClientCharacteristic._read_result(conn_handle, value_handle, bytes(char_data))
+    elif event == _IRQ_GATTC_READ_DONE:
+        conn_handle, value_handle, status = data
+        ClientCharacteristic._read_done(conn_handle, value_handle, status)
+    elif event == _IRQ_GATTC_WRITE_DONE:
+        conn_handle, value_handle, status = data
+        ClientCharacteristic._write_done(conn_handle, value_handle, status)
+    elif event == _IRQ_GATTC_NOTIFY:
+        conn_handle, value_handle, notify_data = data
+        ClientCharacteristic._on_notify(conn_handle, value_handle, bytes(notify_data))
+    elif event == _IRQ_GATTC_INDICATE:
+        conn_handle, value_handle, indicate_data = data
+        ClientCharacteristic._on_indicate(conn_handle, value_handle, bytes(indicate_data))
+
+
+register_irq_handler(_client_irq, None)
+
+
+# Async generator for discovering services, characteristics, descriptors.
+class ClientDiscover:
+    def __init__(self, connection, disc_type, parent, timeout_ms, *args):
+        self._connection = connection
+
+        # Each result IRQ will append to this.
+        self._queue = []
+        # This will be set by the done IRQ.
+        self._status = None
+
+        # Tell the generator to process new events.
+        self._event = asyncio.ThreadSafeFlag()
+
+        # Must implement the _start_discovery static method. Instances of this
+        # type are returned by __anext__.
+        self._disc_type = disc_type
+
+        # This will be the connection for a service discovery, and the service for a characteristic discovery.
+        self._parent = parent
+
+        # Timeout for the discovery process.
+        # TODO: Not implemented.
+        self._timeout_ms = timeout_ms
+
+        # Additional arguments to pass to the _start_discovery method on disc_type.
+        self._args = args
+
+    async def _start(self):
+        if self._connection._discover:
+            # TODO: cancel existing? (e.g. perhaps they didn't let the loop run to completion)
+            raise ValueError("Discovery in progress")
+
+        # Tell the connection that we're the active discovery operation (the IRQ only gives us conn_handle).
+        self._connection._discover = self
+        # Call the appropriate ubluetooth.BLE method.
+        self._disc_type._start_discovery(self._parent, *self._args)
+
+    def __aiter__(self):
+        return self
+
+    async def __anext__(self):
+        if self._connection._discover != self:
+            # Start the discovery if necessary.
+            await self._start()
+
+        # Keep returning items from the queue until the status is set by the
+        # done IRQ.
+        while True:
+            while self._queue:
+                return self._disc_type(self._parent, *self._queue.pop())
+            if self._status is not None:
+                self._connection._discover = None
+                raise StopAsyncIteration
+            # Wait for more results to be added to the queue.
+            await self._event.wait()
+
+    # Tell the active discovery instance for this connection to add a new result
+    # to the queue.
+    def _discover_result(conn_handle, *args):
+        if connection := DeviceConnection._connected.get(conn_handle, None):
+            if discover := connection._discover:
+                discover._queue.append(args)
+                discover._event.set()
+
+    # Tell the active discovery instance for this connection that it is complete.
+    def _discover_done(conn_handle, status):
+        if connection := DeviceConnection._connected.get(conn_handle, None):
+            if discover := connection._discover:
+                discover._status = status
+                discover._event.set()
+
+
+# Represents a single service supported by a connection. Do not construct this
+# class directly, instead use `async for service in connection.services([uuid])` or
+# `await connection.service(uuid)`.
+class ClientService:
+    def __init__(self, connection, start_handle, end_handle, uuid):
+        self.connection = connection
+
+        # Used for characteristic discovery.
+        self._start_handle = start_handle
+        self._end_handle = end_handle
+
+        # Allows comparison to a known uuid.
+        self.uuid = uuid
+
+    def __str__(self):
+        return "Service: {} {} {}".format(self._start_handle, self._end_handle, self.uuid)
+
+    # Search for a specific characteristic by uuid.
+    async def characteristic(self, uuid, timeout_ms=2000):
+        result = None
+        # Make sure loop runs to completion.
+        async for characteristic in self.characteristics(uuid, timeout_ms):
+            if not result and characteristic.uuid == uuid:
+                # Keep first result.
+                result = characteristic
+        return result
+
+    # Search for all services (optionally by uuid).
+    # Use with `async for`, e.g.
+    #     async for characteristic in service.characteristics():
+    # Note: must allow the loop to run to completion.
+    def characteristics(self, uuid=None, timeout_ms=2000):
+        return ClientDiscover(self.connection, ClientCharacteristic, self, timeout_ms, uuid)
+
+    # For ClientDiscover
+    def _start_discovery(connection, uuid=None):
+        ble.gattc_discover_services(connection._conn_handle, uuid)
+
+
+class BaseClientCharacteristic:
+    def __init__(self, value_handle, properties, uuid):
+        # Used for read/write/notify ops.
+        self._value_handle = value_handle
+
+        # Which operations are supported.
+        self.properties = properties
+
+        # Allows comparison to a known uuid.
+        self.uuid = uuid
+
+        if properties & _FLAG_READ:
+            # Fired for each read result and read done IRQ.
+            self._read_event = None
+            self._read_data = None
+            # Used to indicate that the read is complete.
+            self._read_status = None
+
+        if (properties & _FLAG_WRITE) or (properties & _FLAG_WRITE_NO_RESPONSE):
+            # Fired for the write done IRQ.
+            self._write_event = None
+            # Used to indicate that the write is complete.
+            self._write_status = None
+
+    # Register this value handle so events can find us.
+    def _register_with_connection(self):
+        self._connection()._characteristics[self._value_handle] = self
+
+    # Map an incoming IRQ to an registered characteristic.
+    def _find(conn_handle, value_handle):
+        if connection := DeviceConnection._connected.get(conn_handle, None):
+            if characteristic := connection._characteristics.get(value_handle, None):
+                return characteristic
+            else:
+                # IRQ for a characteristic that we weren't expecting. e.g.
+                # notification when we're not waiting on notified().
+                # TODO: This will happen on btstack, which doesn't give us
+                # value handle for the done event.
+                return None
+
+    def _check(self, flag):
+        if not (self.properties & flag):
+            raise ValueError("Unsupported")
+
+    # Issue a read to the characteristic.
+    async def read(self, timeout_ms=1000):
+        self._check(_FLAG_READ)
+        # Make sure this conn_handle/value_handle is known.
+        self._register_with_connection()
+        # This will be set by the done IRQ.
+        self._read_status = None
+        # This will be set by the result and done IRQs. Re-use if possible.
+        self._read_event = self._read_event or asyncio.ThreadSafeFlag()
+
+        # Issue the read.
+        ble.gattc_read(self._connection()._conn_handle, self._value_handle)
+
+        with self._connection().timeout(timeout_ms):
+            # The event will be set for each read result, then a final time for done.
+            while self._read_status is None:
+                await self._read_event.wait()
+            if self._read_status != 0:
+                raise GattError(self._read_status)
+            return self._read_data
+
+    # Map an incoming result IRQ to a registered characteristic.
+    def _read_result(conn_handle, value_handle, data):
+        if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
+            characteristic._read_data = data
+            characteristic._read_event.set()
+
+    # Map an incoming read done IRQ to a registered characteristic.
+    def _read_done(conn_handle, value_handle, status):
+        if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
+            characteristic._read_status = status
+            characteristic._read_event.set()
+
+    async def write(self, data, response=None, timeout_ms=1000):
+        self._check(_FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE)
+
+        # If the response arg is unset, then default it to true if we only support write-with-response.
+        if response is None:
+            p = self.properties
+            response = (p & _FLAG_WRITE) and not (p & _FLAG_WRITE_NO_RESPONSE)
+
+        if response:
+            # Same as read.
+            self._register_with_connection()
+            self._write_status = None
+            self._write_event = self._write_event or asyncio.ThreadSafeFlag()
+
+        # Issue the write.
+        ble.gattc_write(self._connection()._conn_handle, self._value_handle, data, response)
+
+        if response:
+            with self._connection().timeout(timeout_ms):
+                # The event will be set for the write done IRQ.
+                await self._write_event.wait()
+                if self._write_status != 0:
+                    raise GattError(self._write_status)
+
+    # Map an incoming write done IRQ to a registered characteristic.
+    def _write_done(conn_handle, value_handle, status):
+        if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
+            characteristic._write_status = status
+            characteristic._write_event.set()
+
+
+# Represents a single characteristic supported by a service. Do not construct
+# this class directly, instead use `async for characteristic in
+# service.characteristics([uuid])` or `await service.characteristic(uuid)`.
+class ClientCharacteristic(BaseClientCharacteristic):
+    def __init__(self, service, end_handle, value_handle, properties, uuid):
+        self.service = service
+        self.connection = service.connection
+
+        # Used for descriptor discovery. If available, otherwise assume just
+        # past the value handle (enough for two descriptors without risking
+        # going into the next characteristic).
+        self._end_handle = end_handle if end_handle > value_handle else value_handle + 2
+
+        super().__init__(value_handle, properties, uuid)
+
+        if properties & _FLAG_NOTIFY:
+            # Fired when a notification arrives.
+            self._notify_event = asyncio.ThreadSafeFlag()
+            # Data for the most recent notification.
+            self._notify_queue = deque((), 1)
+        if properties & _FLAG_INDICATE:
+            # Same for indications.
+            self._indicate_event = asyncio.ThreadSafeFlag()
+            self._indicate_queue = deque((), 1)
+
+    def __str__(self):
+        return "Characteristic: {} {} {} {}".format(
+            self._end_handle, self._value_handle, self.properties, self.uuid
+        )
+
+    def _connection(self):
+        return self.service.connection
+
+    # Search for a specific descriptor by uuid.
+    async def descriptor(self, uuid, timeout_ms=2000):
+        result = None
+        # Make sure loop runs to completion.
+        async for descriptor in self.descriptors(timeout_ms):
+            if not result and descriptor.uuid == uuid:
+                # Keep first result.
+                result = descriptor
+        return result
+
+    # Search for all services (optionally by uuid).
+    # Use with `async for`, e.g.
+    #     async for descriptor in characteristic.descriptors():
+    # Note: must allow the loop to run to completion.
+    def descriptors(self, timeout_ms=2000):
+        return ClientDiscover(self.connection, ClientDescriptor, self, timeout_ms)
+
+    # For ClientDiscover
+    def _start_discovery(service, uuid=None):
+        ble.gattc_discover_characteristics(
+            service.connection._conn_handle,
+            service._start_handle,
+            service._end_handle,
+            uuid,
+        )
+
+    # Helper for notified() and indicated().
+    async def _notified_indicated(self, queue, event, timeout_ms):
+        # Ensure that events for this connection can route to this characteristic.
+        self._register_with_connection()
+
+        # If the queue is empty, then we need to wait. However, if the queue
+        # has a single item, we also need to do a no-op wait in order to
+        # clear the event flag (because the queue will become empty and
+        # therefore the event should be cleared).
+        if len(queue) <= 1:
+            with self._connection().timeout(timeout_ms):
+                await event.wait()
+
+        # Either we started > 1 item, or the wait completed successfully, return
+        # the front of the queue.
+        return queue.popleft()
+
+    # Wait for the next notification.
+    # Will return immediately if a notification has already been received.
+    async def notified(self, timeout_ms=None):
+        self._check(_FLAG_NOTIFY)
+        return await self._notified_indicated(self._notify_queue, self._notify_event, timeout_ms)
+
+    def _on_notify_indicate(self, queue, event, data):
+        # If we've gone from empty to one item, then wake something
+        # blocking on `await char.notified()` (or `await char.indicated()`).
+        wake = len(queue) == 0
+        # Append the data. By default this is a deque with max-length==1, so it
+        # replaces. But if capture is enabled then it will append.
+        queue.append(data)
+        if wake:
+            # Queue is now non-empty. If something is waiting, it will be
+            # worken. If something isn't waiting right now, then a future
+            # caller to `await char.written()` will see the queue is
+            # non-empty, and wait on the event if it's going to empty the
+            # queue.
+            event.set()
+
+    # Map an incoming notify IRQ to a registered characteristic.
+    def _on_notify(conn_handle, value_handle, notify_data):
+        if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
+            characteristic._on_notify_indicate(
+                characteristic._notify_queue, characteristic._notify_event, notify_data
+            )
+
+    # Wait for the next indication.
+    # Will return immediately if an indication has already been received.
+    async def indicated(self, timeout_ms=None):
+        self._check(_FLAG_INDICATE)
+        return await self._notified_indicated(
+            self._indicate_queue, self._indicate_event, timeout_ms
+        )
+
+    # Map an incoming indicate IRQ to a registered characteristic.
+    def _on_indicate(conn_handle, value_handle, indicate_data):
+        if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
+            characteristic._on_notify_indicate(
+                characteristic._indicate_queue, characteristic._indicate_event, indicate_data
+            )
+
+    # Write to the Client Characteristic Configuration to subscribe to
+    # notify/indications for this characteristic.
+    async def subscribe(self, notify=True, indicate=False):
+        # Ensure that the generated notifications are dispatched in case the app
+        # hasn't awaited on notified/indicated yet.
+        self._register_with_connection()
+        if cccd := await self.descriptor(bluetooth.UUID(_CCCD_UUID)):
+            await cccd.write(struct.pack("<H", _CCCD_NOTIFY * notify + _CCCD_INDICATE * indicate))
+        else:
+            raise ValueError("CCCD not found")
+
+
+# Represents a single descriptor supported by a characteristic. Do not construct
+# this class directly, instead use `async for descriptors in
+# characteristic.descriptors([uuid])` or `await characteristic.descriptor(uuid)`.
+class ClientDescriptor(BaseClientCharacteristic):
+    def __init__(self, characteristic, dsc_handle, uuid):
+        self.characteristic = characteristic
+
+        super().__init__(dsc_handle, _FLAG_READ | _FLAG_WRITE, uuid)
+
+    def __str__(self):
+        return "Descriptor: {} {} {}".format(self._value_handle, self.properties, self.uuid)
+
+    def _connection(self):
+        return self.characteristic.service.connection
+
+    # For ClientDiscover
+    def _start_discovery(characteristic, uuid=None):
+        ble.gattc_discover_descriptors(
+            characteristic._connection()._conn_handle,
+            characteristic._value_handle,
+            characteristic._end_handle,
+        )
diff --git a/micropython/bluetooth/aioble/aioble/core.py b/micropython/bluetooth/aioble/aioble/core.py
new file mode 100644
index 000000000..8daa2446a
--- /dev/null
+++ b/micropython/bluetooth/aioble/aioble/core.py
@@ -0,0 +1,78 @@
+# MicroPython aioble module
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+import bluetooth
+
+
+log_level = 1
+
+
+def log_error(*args):
+    if log_level > 0:
+        print("[aioble] E:", *args)
+
+
+def log_warn(*args):
+    if log_level > 1:
+        print("[aioble] W:", *args)
+
+
+def log_info(*args):
+    if log_level > 2:
+        print("[aioble] I:", *args)
+
+
+class GattError(Exception):
+    def __init__(self, status):
+        self._status = status
+
+
+def ensure_active():
+    if not ble.active():
+        try:
+            from .security import load_secrets
+
+            load_secrets()
+        except:
+            pass
+        ble.active(True)
+
+
+def config(*args, **kwargs):
+    ensure_active()
+    return ble.config(*args, **kwargs)
+
+
+# Because different functionality is enabled by which files are available the
+# different modules can register their IRQ handlers and shutdown handlers
+# dynamically.
+_irq_handlers = []
+_shutdown_handlers = []
+
+
+def register_irq_handler(irq, shutdown):
+    if irq:
+        _irq_handlers.append(irq)
+    if shutdown:
+        _shutdown_handlers.append(shutdown)
+
+
+def stop():
+    ble.active(False)
+    for handler in _shutdown_handlers:
+        handler()
+
+
+# Dispatch IRQs to the registered sub-modules.
+def ble_irq(event, data):
+    log_info(event, data)
+
+    for handler in _irq_handlers:
+        result = handler(event, data)
+        if result is not None:
+            return result
+
+
+# TODO: Allow this to be injected.
+ble = bluetooth.BLE()
+ble.irq(ble_irq)
diff --git a/micropython/bluetooth/aioble/aioble/device.py b/micropython/bluetooth/aioble/aioble/device.py
new file mode 100644
index 000000000..93819bc1e
--- /dev/null
+++ b/micropython/bluetooth/aioble/aioble/device.py
@@ -0,0 +1,304 @@
+# MicroPython aioble module
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+from micropython import const
+
+import asyncio
+import binascii
+
+from .core import ble, register_irq_handler, log_error
+
+
+_IRQ_MTU_EXCHANGED = const(21)
+
+
+# Raised by `with device.timeout()`.
+class DeviceDisconnectedError(Exception):
+    pass
+
+
+def _device_irq(event, data):
+    if event == _IRQ_MTU_EXCHANGED:
+        conn_handle, mtu = data
+        if device := DeviceConnection._connected.get(conn_handle, None):
+            device.mtu = mtu
+            if device._mtu_event:
+                device._mtu_event.set()
+
+
+register_irq_handler(_device_irq, None)
+
+
+# Context manager to allow an operation to be cancelled by timeout or device
+# disconnection. Don't use this directly -- use `with connection.timeout(ms):`
+# instead.
+class DeviceTimeout:
+    def __init__(self, connection, timeout_ms):
+        self._connection = connection
+        self._timeout_ms = timeout_ms
+
+        # We allow either (or both) connection and timeout_ms to be None. This
+        # allows this to be used either as a just-disconnect, just-timeout, or
+        # no-op.
+
+        # This task is active while the operation is in progress. It sleeps
+        # until the timeout, and then cancels the working task. If the working
+        # task completes, __exit__ will cancel the sleep.
+        self._timeout_task = None
+
+        # This is the task waiting for the actual operation to complete.
+        # Usually this is waiting on an event that will be set() by an IRQ
+        # handler.
+        self._task = asyncio.current_task()
+
+        # Tell the connection that if it disconnects, it should cancel this
+        # operation (by cancelling self._task).
+        if connection:
+            connection._timeouts.append(self)
+
+    async def _timeout_sleep(self):
+        try:
+            await asyncio.sleep_ms(self._timeout_ms)
+        except asyncio.CancelledError:
+            # The operation completed successfully and this timeout task was
+            # cancelled by __exit__.
+            return
+
+        # The sleep completed, so we should trigger the timeout. Set
+        # self._timeout_task to None so that we can tell the difference
+        # between a disconnect and a timeout in __exit__.
+        self._timeout_task = None
+        self._task.cancel()
+
+    def __enter__(self):
+        if self._timeout_ms:
+            # Schedule the timeout waiter.
+            self._timeout_task = asyncio.create_task(self._timeout_sleep())
+
+    def __exit__(self, exc_type, exc_val, exc_traceback):
+        # One of five things happened:
+        # 1 - The operation completed successfully.
+        # 2 - The operation timed out.
+        # 3 - The device disconnected.
+        # 4 - The operation failed for a different exception.
+        # 5 - The task was cancelled by something else.
+
+        # Don't need the connection to tell us about disconnection anymore.
+        if self._connection:
+            self._connection._timeouts.remove(self)
+
+        try:
+            if exc_type == asyncio.CancelledError:
+                # Case 2, we started a timeout and it's completed.
+                if self._timeout_ms and self._timeout_task is None:
+                    raise asyncio.TimeoutError
+
+                # Case 3, we have a disconnected device.
+                if self._connection and self._connection._conn_handle is None:
+                    raise DeviceDisconnectedError
+
+                # Case 5, something else cancelled us.
+                # Allow the cancellation to propagate.
+                return
+
+            # Case 1 & 4. Either way, just stop the timeout task and let the
+            # exception (if case 4) propagate.
+        finally:
+            # In all cases, if the timeout is still running, cancel it.
+            if self._timeout_task:
+                self._timeout_task.cancel()
+
+
+class Device:
+    def __init__(self, addr_type, addr):
+        # Public properties
+        self.addr_type = addr_type
+        self.addr = addr if len(addr) == 6 else binascii.unhexlify(addr.replace(":", ""))
+        self._connection = None
+
+    def __eq__(self, rhs):
+        return self.addr_type == rhs.addr_type and self.addr == rhs.addr
+
+    def __hash__(self):
+        return hash((self.addr_type, self.addr))
+
+    def __str__(self):
+        return "Device({}, {}{})".format(
+            "ADDR_PUBLIC" if self.addr_type == 0 else "ADDR_RANDOM",
+            self.addr_hex(),
+            ", CONNECTED" if self._connection else "",
+        )
+
+    def addr_hex(self):
+        return binascii.hexlify(self.addr, ":").decode()
+
+    async def connect(
+        self,
+        timeout_ms=10000,
+        scan_duration_ms=None,
+        min_conn_interval_us=None,
+        max_conn_interval_us=None,
+    ):
+        if self._connection:
+            return self._connection
+
+        # Forward to implementation in central.py.
+        from .central import _connect
+
+        await _connect(
+            DeviceConnection(self),
+            timeout_ms,
+            scan_duration_ms,
+            min_conn_interval_us,
+            max_conn_interval_us,
+        )
+
+        # Start the device task that will clean up after disconnection.
+        self._connection._run_task()
+        return self._connection
+
+
+class DeviceConnection:
+    # Global map of connection handle to active devices (for IRQ mapping).
+    _connected = {}
+
+    def __init__(self, device):
+        self.device = device
+        device._connection = self
+
+        self.encrypted = False
+        self.authenticated = False
+        self.bonded = False
+        self.key_size = False
+        self.mtu = None
+
+        self._conn_handle = None
+
+        # This event is fired by the IRQ both for connection and disconnection
+        # and controls the device_task.
+        self._event = asyncio.ThreadSafeFlag()
+
+        # If we're waiting for a pending MTU exchange.
+        self._mtu_event = None
+
+        # In-progress client discovery instance (e.g. services, chars,
+        # descriptors) used for IRQ mapping.
+        self._discover = None
+        # Map of value handle to characteristic (so that IRQs with
+        # conn_handle,value_handle can route to them). See
+        # ClientCharacteristic._find for where this is used.
+        self._characteristics = {}
+
+        self._task = None
+
+        # DeviceTimeout instances that are currently waiting on this device
+        # and need to be notified if disconnection occurs.
+        self._timeouts = []
+
+        # Fired by the encryption update event.
+        self._pair_event = None
+
+        # Active L2CAP channel for this device.
+        # TODO: Support more than one concurrent channel.
+        self._l2cap_channel = None
+
+    # While connected, this tasks waits for disconnection then cleans up.
+    async def device_task(self):
+        assert self._conn_handle is not None
+
+        # Wait for the (either central or peripheral) disconnected irq.
+        await self._event.wait()
+
+        # Mark the device as disconnected.
+        del DeviceConnection._connected[self._conn_handle]
+        self._conn_handle = None
+        self.device._connection = None
+
+        # Cancel any in-progress operations on this device.
+        for t in self._timeouts:
+            t._task.cancel()
+
+    def _run_task(self):
+        self._task = asyncio.create_task(self.device_task())
+
+    async def disconnect(self, timeout_ms=2000):
+        await self.disconnected(timeout_ms, disconnect=True)
+
+    async def disconnected(self, timeout_ms=None, disconnect=False):
+        if not self.is_connected():
+            return
+
+        # The task must have been created after successful connection.
+        assert self._task
+
+        if disconnect:
+            try:
+                ble.gap_disconnect(self._conn_handle)
+            except OSError as e:
+                log_error("Disconnect", e)
+
+        with DeviceTimeout(None, timeout_ms):
+            await self._task
+
+    # Retrieve a single service matching this uuid.
+    async def service(self, uuid, timeout_ms=2000):
+        result = None
+        # Make sure loop runs to completion.
+        async for service in self.services(uuid, timeout_ms):
+            if not result and service.uuid == uuid:
+                result = service
+        return result
+
+    # Search for all services (optionally by uuid).
+    # Use with `async for`, e.g.
+    #     async for service in device.services():
+    # Note: must allow the loop to run to completion.
+    # TODO: disconnection / timeout
+    def services(self, uuid=None, timeout_ms=2000):
+        from .client import ClientDiscover, ClientService
+
+        return ClientDiscover(self, ClientService, self, timeout_ms, uuid)
+
+    async def pair(self, *args, **kwargs):
+        from .security import pair
+
+        await pair(self, *args, **kwargs)
+
+    def is_connected(self):
+        return self._conn_handle is not None
+
+    # Use with `with` to simplify disconnection and timeout handling.
+    def timeout(self, timeout_ms):
+        return DeviceTimeout(self, timeout_ms)
+
+    async def exchange_mtu(self, mtu=None, timeout_ms=1000):
+        if not self.is_connected():
+            raise ValueError("Not connected")
+
+        if mtu:
+            ble.config(mtu=mtu)
+
+        self._mtu_event = self._mtu_event or asyncio.ThreadSafeFlag()
+        ble.gattc_exchange_mtu(self._conn_handle)
+        with self.timeout(timeout_ms):
+            await self._mtu_event.wait()
+        return self.mtu
+
+    # Wait for a connection on an L2CAP connection-oriented-channel.
+    async def l2cap_accept(self, psm, mtu, timeout_ms=None):
+        from .l2cap import accept
+
+        return await accept(self, psm, mtu, timeout_ms)
+
+    # Attempt to connect to a listening device.
+    async def l2cap_connect(self, psm, mtu, timeout_ms=1000):
+        from .l2cap import connect
+
+        return await connect(self, psm, mtu, timeout_ms)
+
+    # Context manager -- automatically disconnect.
+    async def __aenter__(self):
+        return self
+
+    async def __aexit__(self, exc_type, exc_val, exc_traceback):
+        await self.disconnect()
diff --git a/micropython/bluetooth/aioble/aioble/l2cap.py b/micropython/bluetooth/aioble/aioble/l2cap.py
new file mode 100644
index 000000000..397b7ceb6
--- /dev/null
+++ b/micropython/bluetooth/aioble/aioble/l2cap.py
@@ -0,0 +1,214 @@
+# MicroPython aioble module
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+from micropython import const
+
+import asyncio
+
+from .core import ble, log_error, register_irq_handler
+from .device import DeviceConnection
+
+
+_IRQ_L2CAP_ACCEPT = const(22)
+_IRQ_L2CAP_CONNECT = const(23)
+_IRQ_L2CAP_DISCONNECT = const(24)
+_IRQ_L2CAP_RECV = const(25)
+_IRQ_L2CAP_SEND_READY = const(26)
+
+
+# Once we start listening we're listening forever. (Limitation in NimBLE)
+_listening = False
+
+
+def _l2cap_irq(event, data):
+    if event not in (
+        _IRQ_L2CAP_CONNECT,
+        _IRQ_L2CAP_DISCONNECT,
+        _IRQ_L2CAP_RECV,
+        _IRQ_L2CAP_SEND_READY,
+    ):
+        return
+
+    # All the L2CAP events start with (conn_handle, cid, ...)
+    if connection := DeviceConnection._connected.get(data[0], None):
+        if channel := connection._l2cap_channel:
+            # Expect to match the cid for this conn handle (unless we're
+            # waiting for connection in which case channel._cid is None).
+            if channel._cid is not None and channel._cid != data[1]:
+                return
+
+            # Update the channel object with new information.
+            if event == _IRQ_L2CAP_CONNECT:
+                _, channel._cid, _, channel.our_mtu, channel.peer_mtu = data
+            elif event == _IRQ_L2CAP_DISCONNECT:
+                _, _, psm, status = data
+                channel._status = status
+                channel._cid = None
+                connection._l2cap_channel = None
+            elif event == _IRQ_L2CAP_RECV:
+                channel._data_ready = True
+            elif event == _IRQ_L2CAP_SEND_READY:
+                channel._stalled = False
+
+            # Notify channel.
+            channel._event.set()
+
+
+def _l2cap_shutdown():
+    global _listening
+    _listening = False
+
+
+register_irq_handler(_l2cap_irq, _l2cap_shutdown)
+
+
+# The channel was disconnected during a send/recvinto/flush.
+class L2CAPDisconnectedError(Exception):
+    pass
+
+
+# Failed to connect to connection (argument is status).
+class L2CAPConnectionError(Exception):
+    pass
+
+
+class L2CAPChannel:
+    def __init__(self, connection):
+        if not connection.is_connected():
+            raise ValueError("Not connected")
+
+        if connection._l2cap_channel:
+            raise ValueError("Already has channel")
+        connection._l2cap_channel = self
+
+        self._connection = connection
+
+        # Maximum size that the other side can send to us.
+        self.our_mtu = 0
+        # Maximum size that we can send.
+        self.peer_mtu = 0
+
+        # Set back to None on disconnection.
+        self._cid = None
+        # Set during disconnection.
+        self._status = 0
+
+        # If true, must wait for _IRQ_L2CAP_SEND_READY IRQ before sending.
+        self._stalled = False
+
+        # Has received a _IRQ_L2CAP_RECV since the buffer was last emptied.
+        self._data_ready = False
+
+        self._event = asyncio.ThreadSafeFlag()
+
+    def _assert_connected(self):
+        if self._cid is None:
+            raise L2CAPDisconnectedError
+
+    async def recvinto(self, buf, timeout_ms=None):
+        self._assert_connected()
+
+        # Wait until the data_ready flag is set. This flag is only ever set by
+        # the event and cleared by this function.
+        with self._connection.timeout(timeout_ms):
+            while not self._data_ready:
+                await self._event.wait()
+                self._assert_connected()
+
+        self._assert_connected()
+
+        # Extract up to len(buf) bytes from the channel buffer.
+        n = ble.l2cap_recvinto(self._connection._conn_handle, self._cid, buf)
+
+        # Check if there's still remaining data in the channel buffers.
+        self._data_ready = ble.l2cap_recvinto(self._connection._conn_handle, self._cid, None) > 0
+
+        return n
+
+    # Synchronously see if there's data ready.
+    def available(self):
+        self._assert_connected()
+        return self._data_ready
+
+    # Waits until the channel is free and then sends buf.
+    # If the buffer is larger than the MTU it will be sent in chunks.
+    async def send(self, buf, timeout_ms=None, chunk_size=None):
+        offset = 0
+        chunk_size = min(self.our_mtu * 2, self.peer_mtu, chunk_size or self.peer_mtu)
+        mv = memoryview(buf)
+        while offset < len(buf):
+            if self._stalled:
+                await self.flush(timeout_ms)
+            # l2cap_send returns True if you can send immediately.
+            self._assert_connected()
+            self._stalled = not ble.l2cap_send(
+                self._connection._conn_handle,
+                self._cid,
+                mv[offset : offset + chunk_size],
+            )
+            offset += chunk_size
+
+    async def flush(self, timeout_ms=None):
+        self._assert_connected()
+        # Wait for the _stalled flag to be cleared by the IRQ.
+        with self._connection.timeout(timeout_ms):
+            while self._stalled:
+                await self._event.wait()
+                self._assert_connected()
+
+    async def disconnect(self, timeout_ms=1000):
+        if self._cid is None:
+            return
+
+        # Wait for the cid to be cleared by the disconnect IRQ.
+        ble.l2cap_disconnect(self._connection._conn_handle, self._cid)
+        await self.disconnected(timeout_ms)
+
+    async def disconnected(self, timeout_ms=1000):
+        with self._connection.timeout(timeout_ms):
+            while self._cid is not None:
+                await self._event.wait()
+
+    # Context manager -- automatically disconnect.
+    async def __aenter__(self):
+        return self
+
+    async def __aexit__(self, exc_type, exc_val, exc_traceback):
+        await self.disconnect()
+
+
+# Use connection.l2cap_accept() instead of calling this directly.
+async def accept(connection, psm, mtu, timeout_ms):
+    global _listening
+
+    channel = L2CAPChannel(connection)
+
+    # Start the stack listening if necessary.
+    if not _listening:
+        ble.l2cap_listen(psm, mtu)
+        _listening = True
+
+    # Wait for the connect irq from the remote connection.
+    with connection.timeout(timeout_ms):
+        await channel._event.wait()
+        return channel
+
+
+# Use connection.l2cap_connect() instead of calling this directly.
+async def connect(connection, psm, mtu, timeout_ms):
+    if _listening:
+        raise ValueError("Can't connect while listening")
+
+    channel = L2CAPChannel(connection)
+
+    with connection.timeout(timeout_ms):
+        ble.l2cap_connect(connection._conn_handle, psm, mtu)
+
+        # Wait for the connect irq from the remote connection.
+        # If the connection fails, we get a disconnect event (with status) instead.
+        await channel._event.wait()
+
+        if channel._cid is not None:
+            return channel
+        else:
+            raise L2CAPConnectionError(channel._status)
diff --git a/micropython/bluetooth/aioble/aioble/peripheral.py b/micropython/bluetooth/aioble/aioble/peripheral.py
new file mode 100644
index 000000000..d3dda8bcb
--- /dev/null
+++ b/micropython/bluetooth/aioble/aioble/peripheral.py
@@ -0,0 +1,178 @@
+# MicroPython aioble module
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+from micropython import const
+
+import bluetooth
+import struct
+
+import asyncio
+
+from .core import (
+    ensure_active,
+    ble,
+    log_info,
+    log_error,
+    log_warn,
+    register_irq_handler,
+)
+from .device import Device, DeviceConnection, DeviceTimeout
+
+
+_IRQ_CENTRAL_CONNECT = const(1)
+_IRQ_CENTRAL_DISCONNECT = const(2)
+
+
+_ADV_TYPE_FLAGS = const(0x01)
+_ADV_TYPE_NAME = const(0x09)
+_ADV_TYPE_UUID16_COMPLETE = const(0x3)
+_ADV_TYPE_UUID32_COMPLETE = const(0x5)
+_ADV_TYPE_UUID128_COMPLETE = const(0x7)
+_ADV_TYPE_UUID16_MORE = const(0x2)
+_ADV_TYPE_UUID32_MORE = const(0x4)
+_ADV_TYPE_UUID128_MORE = const(0x6)
+_ADV_TYPE_APPEARANCE = const(0x19)
+_ADV_TYPE_MANUFACTURER = const(0xFF)
+
+_ADV_PAYLOAD_MAX_LEN = const(31)
+
+
+_incoming_connection = None
+_connect_event = None
+
+
+def _peripheral_irq(event, data):
+    global _incoming_connection
+
+    if event == _IRQ_CENTRAL_CONNECT:
+        conn_handle, addr_type, addr = data
+
+        # Create, initialise, and register the device.
+        device = Device(addr_type, bytes(addr))
+        _incoming_connection = DeviceConnection(device)
+        _incoming_connection._conn_handle = conn_handle
+        DeviceConnection._connected[conn_handle] = _incoming_connection
+
+        # Signal advertise() to return the connected device.
+        _connect_event.set()
+
+    elif event == _IRQ_CENTRAL_DISCONNECT:
+        conn_handle, _, _ = data
+        if connection := DeviceConnection._connected.get(conn_handle, None):
+            # Tell the device_task that it should terminate.
+            connection._event.set()
+
+
+def _peripheral_shutdown():
+    global _incoming_connection, _connect_event
+    _incoming_connection = None
+    _connect_event = None
+
+
+register_irq_handler(_peripheral_irq, _peripheral_shutdown)
+
+
+# Advertising payloads are repeated packets of the following form:
+#   1 byte data length (N + 1)
+#   1 byte type (see constants below)
+#   N bytes type-specific data
+def _append(adv_data, resp_data, adv_type, value):
+    data = struct.pack("BB", len(value) + 1, adv_type) + value
+
+    if len(data) + len(adv_data) < _ADV_PAYLOAD_MAX_LEN:
+        adv_data += data
+        return resp_data
+
+    if len(data) + (len(resp_data) if resp_data else 0) < _ADV_PAYLOAD_MAX_LEN:
+        if not resp_data:
+            # Overflow into resp_data for the first time.
+            resp_data = bytearray()
+        resp_data += data
+        return resp_data
+
+    raise ValueError("Advertising payload too long")
+
+
+async def advertise(
+    interval_us,
+    adv_data=None,
+    resp_data=None,
+    connectable=True,
+    limited_disc=False,
+    br_edr=False,
+    name=None,
+    services=None,
+    appearance=0,
+    manufacturer=None,
+    timeout_ms=None,
+):
+    global _incoming_connection, _connect_event
+
+    ensure_active()
+
+    if not adv_data and not resp_data:
+        # If the user didn't manually specify adv_data / resp_data then
+        # construct them from the kwargs. Keep adding fields to adv_data,
+        # overflowing to resp_data if necessary.
+        # TODO: Try and do better bin-packing than just concatenating in
+        # order?
+
+        adv_data = bytearray()
+
+        resp_data = _append(
+            adv_data,
+            resp_data,
+            _ADV_TYPE_FLAGS,
+            struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)),
+        )
+
+        # Services are prioritised to go in the advertising data because iOS supports
+        # filtering scan results by service only, so services must come first.
+        if services:
+            for uuid_len, code in (
+                (2, _ADV_TYPE_UUID16_COMPLETE),
+                (4, _ADV_TYPE_UUID32_COMPLETE),
+                (16, _ADV_TYPE_UUID128_COMPLETE),
+            ):
+                if uuids := [bytes(uuid) for uuid in services if len(bytes(uuid)) == uuid_len]:
+                    resp_data = _append(adv_data, resp_data, code, b"".join(uuids))
+
+        if name:
+            resp_data = _append(adv_data, resp_data, _ADV_TYPE_NAME, name)
+
+        if appearance:
+            # See org.bluetooth.characteristic.gap.appearance.xml
+            resp_data = _append(
+                adv_data, resp_data, _ADV_TYPE_APPEARANCE, struct.pack("<H", appearance)
+            )
+
+        if manufacturer:
+            resp_data = _append(
+                adv_data,
+                resp_data,
+                _ADV_TYPE_MANUFACTURER,
+                struct.pack("<H", manufacturer[0]) + manufacturer[1],
+            )
+
+    _connect_event = _connect_event or asyncio.ThreadSafeFlag()
+    ble.gap_advertise(interval_us, adv_data=adv_data, resp_data=resp_data, connectable=connectable)
+
+    try:
+        # Allow optional timeout for a central to connect to us (or just to stop advertising).
+        with DeviceTimeout(None, timeout_ms):
+            await _connect_event.wait()
+
+        # Get the newly connected connection to the central and start a task
+        # to wait for disconnection.
+        result = _incoming_connection
+        _incoming_connection = None
+        # This mirrors what connecting to a central does.
+        result._run_task()
+        return result
+    except asyncio.CancelledError:
+        # Something else cancelled this task (to manually stop advertising).
+        ble.gap_advertise(None)
+    except asyncio.TimeoutError:
+        # DeviceTimeout waiting for connection.
+        ble.gap_advertise(None)
+        raise
diff --git a/micropython/bluetooth/aioble/aioble/security.py b/micropython/bluetooth/aioble/aioble/security.py
new file mode 100644
index 000000000..8e04d5b7b
--- /dev/null
+++ b/micropython/bluetooth/aioble/aioble/security.py
@@ -0,0 +1,178 @@
+# MicroPython aioble module
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+from micropython import const, schedule
+import asyncio
+import binascii
+import json
+
+from .core import log_info, log_warn, ble, register_irq_handler
+from .device import DeviceConnection
+
+_IRQ_ENCRYPTION_UPDATE = const(28)
+_IRQ_GET_SECRET = const(29)
+_IRQ_SET_SECRET = const(30)
+_IRQ_PASSKEY_ACTION = const(31)
+
+_IO_CAPABILITY_DISPLAY_ONLY = const(0)
+_IO_CAPABILITY_DISPLAY_YESNO = const(1)
+_IO_CAPABILITY_KEYBOARD_ONLY = const(2)
+_IO_CAPABILITY_NO_INPUT_OUTPUT = const(3)
+_IO_CAPABILITY_KEYBOARD_DISPLAY = const(4)
+
+_PASSKEY_ACTION_INPUT = const(2)
+_PASSKEY_ACTION_DISP = const(3)
+_PASSKEY_ACTION_NUMCMP = const(4)
+
+_DEFAULT_PATH = "ble_secrets.json"
+
+_secrets = {}
+_modified = False
+_path = None
+
+
+# Must call this before stack startup.
+def load_secrets(path=None):
+    global _path, _secrets
+
+    # Use path if specified, otherwise use previous path, otherwise use
+    # default path.
+    _path = path or _path or _DEFAULT_PATH
+
+    # Reset old secrets.
+    _secrets = {}
+    try:
+        with open(_path, "r") as f:
+            entries = json.load(f)
+            for sec_type, key, value in entries:
+                # Decode bytes from hex.
+                _secrets[sec_type, binascii.a2b_base64(key)] = binascii.a2b_base64(value)
+    except:
+        log_warn("No secrets available")
+
+
+# Call this whenever the secrets dict changes.
+def _save_secrets(arg=None):
+    global _modified, _path
+
+    _path = _path or _DEFAULT_PATH
+
+    if not _modified:
+        # Only save if the secrets changed.
+        return
+
+    with open(_path, "w") as f:
+        # Convert bytes to hex strings (otherwise JSON will treat them like
+        # strings).
+        json_secrets = [
+            (sec_type, binascii.b2a_base64(key), binascii.b2a_base64(value))
+            for (sec_type, key), value in _secrets.items()
+        ]
+        json.dump(json_secrets, f)
+        _modified = False
+
+
+def _security_irq(event, data):
+    global _modified
+
+    if event == _IRQ_ENCRYPTION_UPDATE:
+        # Connection has updated (usually due to pairing).
+        conn_handle, encrypted, authenticated, bonded, key_size = data
+        log_info("encryption update", conn_handle, encrypted, authenticated, bonded, key_size)
+        if connection := DeviceConnection._connected.get(conn_handle, None):
+            connection.encrypted = encrypted
+            connection.authenticated = authenticated
+            connection.bonded = bonded
+            connection.key_size = key_size
+            # TODO: Handle failure.
+            if encrypted and connection._pair_event:
+                connection._pair_event.set()
+
+    elif event == _IRQ_SET_SECRET:
+        sec_type, key, value = data
+        key = sec_type, bytes(key)
+        value = bytes(value) if value else None
+
+        log_info("set secret:", key, value)
+
+        if value is None:
+            # Delete secret.
+            if key not in _secrets:
+                return False
+
+            del _secrets[key]
+        else:
+            # Save secret.
+            _secrets[key] = value
+
+        # Queue up a save (don't synchronously write to flash).
+        _modified = True
+        schedule(_save_secrets, None)
+
+        return True
+
+    elif event == _IRQ_GET_SECRET:
+        sec_type, index, key = data
+
+        log_info("get secret:", sec_type, index, bytes(key) if key else None)
+
+        if key is None:
+            # Return the index'th secret of this type.
+            i = 0
+            for (t, _key), value in _secrets.items():
+                if t == sec_type:
+                    if i == index:
+                        return value
+                    i += 1
+            return None
+        else:
+            # Return the secret for this key (or None).
+            key = sec_type, bytes(key)
+            return _secrets.get(key, None)
+
+    elif event == _IRQ_PASSKEY_ACTION:
+        conn_handle, action, passkey = data
+        log_info("passkey action", conn_handle, action, passkey)
+        # if action == _PASSKEY_ACTION_NUMCMP:
+        #     # TODO: Show this passkey and confirm accept/reject.
+        #     accept = 1
+        #     self._ble.gap_passkey(conn_handle, action, accept)
+        # elif action == _PASSKEY_ACTION_DISP:
+        #     # TODO: Generate and display a passkey so the remote device can enter it.
+        #     passkey = 123456
+        #     self._ble.gap_passkey(conn_handle, action, passkey)
+        # elif action == _PASSKEY_ACTION_INPUT:
+        #     # TODO: Ask the user to enter the passkey shown on the remote device.
+        #     passkey = 123456
+        #     self._ble.gap_passkey(conn_handle, action, passkey)
+        # else:
+        #     log_warn("unknown passkey action")
+
+
+def _security_shutdown():
+    global _secrets, _modified, _path
+    _secrets = {}
+    _modified = False
+    _path = None
+
+
+register_irq_handler(_security_irq, _security_shutdown)
+
+
+# Use device.pair() rather than calling this directly.
+async def pair(
+    connection,
+    bond=True,
+    le_secure=True,
+    mitm=False,
+    io=_IO_CAPABILITY_NO_INPUT_OUTPUT,
+    timeout_ms=20000,
+):
+    ble.config(bond=bond, le_secure=le_secure, mitm=mitm, io=io)
+
+    with connection.timeout(timeout_ms):
+        connection._pair_event = asyncio.ThreadSafeFlag()
+        ble.gap_pair(connection._conn_handle)
+        await connection._pair_event.wait()
+        # TODO: Allow the passkey action to return to here and
+        # invoke a callback or task to process the action.
diff --git a/micropython/bluetooth/aioble/aioble/server.py b/micropython/bluetooth/aioble/aioble/server.py
new file mode 100644
index 000000000..5d5d7399b
--- /dev/null
+++ b/micropython/bluetooth/aioble/aioble/server.py
@@ -0,0 +1,340 @@
+# MicroPython aioble module
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+from micropython import const
+from collections import deque
+import bluetooth
+import asyncio
+
+from .core import (
+    ensure_active,
+    ble,
+    log_info,
+    log_error,
+    log_warn,
+    register_irq_handler,
+    GattError,
+)
+from .device import DeviceConnection, DeviceTimeout
+
+_registered_characteristics = {}
+
+_IRQ_GATTS_WRITE = const(3)
+_IRQ_GATTS_READ_REQUEST = const(4)
+_IRQ_GATTS_INDICATE_DONE = const(20)
+
+_FLAG_READ = const(0x0002)
+_FLAG_WRITE_NO_RESPONSE = const(0x0004)
+_FLAG_WRITE = const(0x0008)
+_FLAG_NOTIFY = const(0x0010)
+_FLAG_INDICATE = const(0x0020)
+
+_FLAG_READ_ENCRYPTED = const(0x0200)
+_FLAG_READ_AUTHENTICATED = const(0x0400)
+_FLAG_READ_AUTHORIZED = const(0x0800)
+_FLAG_WRITE_ENCRYPTED = const(0x1000)
+_FLAG_WRITE_AUTHENTICATED = const(0x2000)
+_FLAG_WRITE_AUTHORIZED = const(0x4000)
+
+_FLAG_WRITE_CAPTURE = const(0x10000)
+
+
+_WRITE_CAPTURE_QUEUE_LIMIT = const(10)
+
+
+def _server_irq(event, data):
+    if event == _IRQ_GATTS_WRITE:
+        conn_handle, attr_handle = data
+        Characteristic._remote_write(conn_handle, attr_handle)
+    elif event == _IRQ_GATTS_READ_REQUEST:
+        conn_handle, attr_handle = data
+        return Characteristic._remote_read(conn_handle, attr_handle)
+    elif event == _IRQ_GATTS_INDICATE_DONE:
+        conn_handle, value_handle, status = data
+        Characteristic._indicate_done(conn_handle, value_handle, status)
+
+
+def _server_shutdown():
+    global _registered_characteristics
+    _registered_characteristics = {}
+    if hasattr(BaseCharacteristic, "_capture_task"):
+        BaseCharacteristic._capture_task.cancel()
+        del BaseCharacteristic._capture_queue
+        del BaseCharacteristic._capture_write_event
+        del BaseCharacteristic._capture_consumed_event
+        del BaseCharacteristic._capture_task
+
+
+register_irq_handler(_server_irq, _server_shutdown)
+
+
+class Service:
+    def __init__(self, uuid):
+        self.uuid = uuid
+        self.characteristics = []
+
+    # Generate tuple for gatts_register_services.
+    def _tuple(self):
+        return (self.uuid, tuple(c._tuple() for c in self.characteristics))
+
+
+class BaseCharacteristic:
+    def _register(self, value_handle):
+        self._value_handle = value_handle
+        _registered_characteristics[value_handle] = self
+        if self._initial is not None:
+            self.write(self._initial)
+            self._initial = None
+
+    # Read value from local db.
+    def read(self):
+        if self._value_handle is None:
+            return self._initial or b""
+        else:
+            return ble.gatts_read(self._value_handle)
+
+    # Write value to local db, and optionally notify/indicate subscribers.
+    def write(self, data, send_update=False):
+        if self._value_handle is None:
+            self._initial = data
+        else:
+            ble.gatts_write(self._value_handle, data, send_update)
+
+    # When the a capture-enabled characteristic is created, create the
+    # necessary events (if not already created).
+    @staticmethod
+    def _init_capture():
+        if hasattr(BaseCharacteristic, "_capture_queue"):
+            return
+
+        BaseCharacteristic._capture_queue = deque((), _WRITE_CAPTURE_QUEUE_LIMIT)
+        BaseCharacteristic._capture_write_event = asyncio.ThreadSafeFlag()
+        BaseCharacteristic._capture_consumed_event = asyncio.ThreadSafeFlag()
+        BaseCharacteristic._capture_task = asyncio.create_task(
+            BaseCharacteristic._run_capture_task()
+        )
+
+    # Monitor the shared queue for incoming characteristic writes and forward
+    # them sequentially to the individual characteristic events.
+    @staticmethod
+    async def _run_capture_task():
+        write = BaseCharacteristic._capture_write_event
+        consumed = BaseCharacteristic._capture_consumed_event
+        q = BaseCharacteristic._capture_queue
+
+        while True:
+            if len(q):
+                conn, data, characteristic = q.popleft()
+                # Let the characteristic waiting in `written()` know that it
+                # can proceed.
+                characteristic._write_data = (conn, data)
+                characteristic._write_event.set()
+                # Wait for the characteristic to complete `written()` before
+                # continuing.
+                await consumed.wait()
+
+            if not len(q):
+                await write.wait()
+
+    # Wait for a write on this characteristic. Returns the connection that did
+    # the write, or a tuple of (connection, value) if capture is enabled for
+    # this characteristics.
+    async def written(self, timeout_ms=None):
+        if not hasattr(self, "_write_event"):
+            # Not a writable characteristic.
+            return
+
+        # If no write has been seen then we need to wait. If the event has
+        # already been set this will clear the event and continue
+        # immediately. In regular mode, this is set by the write IRQ
+        # directly (in _remote_write). In capture mode, this is set when it's
+        # our turn by _capture_task.
+        with DeviceTimeout(None, timeout_ms):
+            await self._write_event.wait()
+
+        # Return the write data and clear the stored copy.
+        # In default usage this will be just the connection handle.
+        # In capture mode this will be a tuple of (connection_handle, received_data)
+        data = self._write_data
+        self._write_data = None
+
+        if self.flags & _FLAG_WRITE_CAPTURE:
+            # Notify the shared queue monitor that the event has been consumed
+            # by the caller to `written()` and another characteristic can now
+            # proceed.
+            BaseCharacteristic._capture_consumed_event.set()
+
+        return data
+
+    def on_read(self, connection):
+        return 0
+
+    def _remote_write(conn_handle, value_handle):
+        if characteristic := _registered_characteristics.get(value_handle, None):
+            # If we've gone from empty to one item, then wake something
+            # blocking on `await char.written()`.
+
+            conn = DeviceConnection._connected.get(conn_handle, None)
+
+            if characteristic.flags & _FLAG_WRITE_CAPTURE:
+                # For capture, we append the connection and the written value
+                # value to the shared queue along with the matching characteristic object.
+                # The deque will enforce the max queue len.
+                data = characteristic.read()
+                BaseCharacteristic._capture_queue.append((conn, data, characteristic))
+                BaseCharacteristic._capture_write_event.set()
+            else:
+                # Store the write connection handle to be later used to retrieve the data
+                # then set event to handle in written() task.
+                characteristic._write_data = conn
+                characteristic._write_event.set()
+
+    def _remote_read(conn_handle, value_handle):
+        if characteristic := _registered_characteristics.get(value_handle, None):
+            return characteristic.on_read(DeviceConnection._connected.get(conn_handle, None))
+
+
+class Characteristic(BaseCharacteristic):
+    def __init__(
+        self,
+        service,
+        uuid,
+        read=False,
+        write=False,
+        write_no_response=False,
+        notify=False,
+        indicate=False,
+        initial=None,
+        capture=False,
+    ):
+        service.characteristics.append(self)
+        self.descriptors = []
+
+        flags = 0
+        if read:
+            flags |= _FLAG_READ
+        if write or write_no_response:
+            flags |= (_FLAG_WRITE if write else 0) | (
+                _FLAG_WRITE_NO_RESPONSE if write_no_response else 0
+            )
+            if capture:
+                # Capture means that we keep track of all writes, and capture
+                # their values (and connection) in a queue. Otherwise we just
+                # track the connection of the most recent write.
+                flags |= _FLAG_WRITE_CAPTURE
+                BaseCharacteristic._init_capture()
+
+            # Set when this characteristic has a value waiting in self._write_data.
+            self._write_event = asyncio.ThreadSafeFlag()
+            # The connection of the most recent write, or a tuple of
+            # (connection, data) if capture is enabled.
+            self._write_data = None
+        if notify:
+            flags |= _FLAG_NOTIFY
+        if indicate:
+            flags |= _FLAG_INDICATE
+            # TODO: This should probably be a dict of connection to (ev, status).
+            # Right now we just support a single indication at a time.
+            self._indicate_connection = None
+            self._indicate_event = asyncio.ThreadSafeFlag()
+            self._indicate_status = None
+
+        self.uuid = uuid
+        self.flags = flags
+        self._value_handle = None
+        self._initial = initial
+
+    # Generate tuple for gatts_register_services.
+    def _tuple(self):
+        if self.descriptors:
+            return (self.uuid, self.flags, tuple(d._tuple() for d in self.descriptors))
+        else:
+            # Workaround: v1.19 and below can't handle an empty descriptor tuple.
+            return (self.uuid, self.flags)
+
+    def notify(self, connection, data=None):
+        if not (self.flags & _FLAG_NOTIFY):
+            raise ValueError("Not supported")
+        ble.gatts_notify(connection._conn_handle, self._value_handle, data)
+
+    async def indicate(self, connection, data=None, timeout_ms=1000):
+        if not (self.flags & _FLAG_INDICATE):
+            raise ValueError("Not supported")
+        if self._indicate_connection is not None:
+            raise ValueError("In progress")
+        if not connection.is_connected():
+            raise ValueError("Not connected")
+
+        self._indicate_connection = connection
+        self._indicate_status = None
+
+        try:
+            with connection.timeout(timeout_ms):
+                ble.gatts_indicate(connection._conn_handle, self._value_handle, data)
+                await self._indicate_event.wait()
+                if self._indicate_status != 0:
+                    raise GattError(self._indicate_status)
+        finally:
+            self._indicate_connection = None
+
+    def _indicate_done(conn_handle, value_handle, status):
+        if characteristic := _registered_characteristics.get(value_handle, None):
+            if connection := DeviceConnection._connected.get(conn_handle, None):
+                if not characteristic._indicate_connection:
+                    # Timeout.
+                    return
+                # See TODO in __init__ to support multiple concurrent indications.
+                assert connection == characteristic._indicate_connection
+                characteristic._indicate_status = status
+                characteristic._indicate_event.set()
+
+
+class BufferedCharacteristic(Characteristic):
+    def __init__(self, *args, max_len=20, append=False, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._max_len = max_len
+        self._append = append
+
+    def _register(self, value_handle):
+        super()._register(value_handle)
+        ble.gatts_set_buffer(value_handle, self._max_len, self._append)
+
+
+class Descriptor(BaseCharacteristic):
+    def __init__(self, characteristic, uuid, read=False, write=False, initial=None):
+        characteristic.descriptors.append(self)
+
+        flags = 0
+        if read:
+            flags |= _FLAG_READ
+        if write:
+            flags |= _FLAG_WRITE
+            self._write_event = asyncio.ThreadSafeFlag()
+            self._write_data = None
+
+        self.uuid = uuid
+        self.flags = flags
+        self._value_handle = None
+        self._initial = initial
+
+    # Generate tuple for gatts_register_services.
+    def _tuple(self):
+        return (self.uuid, self.flags)
+
+
+# Turn the Service/Characteristic/Descriptor classes into a registration tuple
+# and then extract their value handles.
+def register_services(*services):
+    ensure_active()
+    _registered_characteristics.clear()
+    handles = ble.gatts_register_services(tuple(s._tuple() for s in services))
+    for i in range(len(services)):
+        service_handles = handles[i]
+        service = services[i]
+        n = 0
+        for characteristic in service.characteristics:
+            characteristic._register(service_handles[n])
+            n += 1
+            for descriptor in characteristic.descriptors:
+                descriptor._register(service_handles[n])
+                n += 1
diff --git a/micropython/bluetooth/aioble/examples/l2cap_file_client.py b/micropython/bluetooth/aioble/examples/l2cap_file_client.py
new file mode 100644
index 000000000..0817ca162
--- /dev/null
+++ b/micropython/bluetooth/aioble/examples/l2cap_file_client.py
@@ -0,0 +1,140 @@
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+# This is a WIP client for l2cap_file_server.py. See that file for more
+# information.
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+
+import asyncio
+import aioble
+import bluetooth
+
+import random
+import struct
+
+_FILE_SERVICE_UUID = bluetooth.UUID(0x1234)
+_CONTROL_CHARACTERISTIC_UUID = bluetooth.UUID(0x1235)
+
+
+_COMMAND_SEND = const(0)
+_COMMAND_RECV = const(1)  # Not yet implemented.
+_COMMAND_LIST = const(2)
+_COMMAND_SIZE = const(3)
+_COMMAND_DONE = const(4)
+
+_STATUS_OK = const(0)
+_STATUS_NOT_IMPLEMENTED = const(1)
+_STATUS_NOT_FOUND = const(2)
+
+_L2CAP_PSN = const(22)
+_L2CAP_MTU = const(128)
+
+
+class FileClient:
+    def __init__(self, device):
+        self._device = device
+        self._connection = None
+        self._seq = 1
+
+    async def connect(self):
+        try:
+            print("Connecting to", self._device)
+            self._connection = await self._device.connect()
+        except asyncio.TimeoutError:
+            print("Timeout during connection")
+            return
+
+        try:
+            print("Discovering...")
+            file_service = await self._connection.service(_FILE_SERVICE_UUID)
+            self._control_characteristic = await file_service.characteristic(
+                _CONTROL_CHARACTERISTIC_UUID
+            )
+        except asyncio.TimeoutError:
+            print("Timeout discovering services/characteristics")
+            return
+
+        print("Connecting channel")
+        self._channel = await self._connection.l2cap_connect(_L2CAP_PSN, _L2CAP_MTU)
+
+    async def _command(self, cmd, data):
+        send_seq = self._seq
+        await self._control_characteristic.write(struct.pack("<BB", cmd, send_seq) + data)
+        self._seq += 1
+        return send_seq
+
+    async def size(self, path):
+        print("Getting size")
+        send_seq = await self._command(_COMMAND_SIZE, path.encode())
+
+        data = await self._control_characteristic.notified()
+        if len(data) != 6:
+            raise RuntimeError("Invalid response")
+
+        seq, status, size = struct.unpack("<BBI", data)
+        if seq != send_seq:
+            raise RuntimeError("Wrong reply")
+
+        print("result:", seq, status, size)
+        return size
+
+    async def download(self, path, dest):
+        size = await self.size(path)
+
+        await self._command(_COMMAND_SEND, path.encode())
+
+        with open(dest, "wb") as f:  # noqa: ASYNC230
+            total = 0
+            buf = bytearray(self._channel.our_mtu)
+            mv = memoryview(buf)
+            while total < size:
+                n = await self._channel.recvinto(buf)
+                f.write(mv[:n])
+                total += n
+
+    async def list(self, path):
+        await self._command(_COMMAND_LIST, path.encode())
+        results = bytearray()
+        buf = bytearray(self._channel.our_mtu)
+        mv = memoryview(buf)
+        while True:
+            n = await self._channel.recvinto(buf)
+            results += mv[:n]
+            if results[len(results) - 1] == ord("\n") and results[len(results) - 2] == ord("\n"):
+                break
+        print(results.decode().split("\n"))
+
+    async def disconnect(self):
+        if self._connection:
+            await self._connection.disconnect()
+
+
+async def main():
+    async with aioble.scan(5000, 30000, 30000, active=True) as scanner:
+        async for result in scanner:
+            if result.name() == "mpy-file" and _FILE_SERVICE_UUID in result.services():
+                device = result.device
+                break
+        else:
+            print("File server not found")
+            return
+
+    client = FileClient(device)
+
+    await client.connect()
+    print(await client.size("demo/file.txt"))
+    print(await client.size("demo/notfound.bin"))
+
+    await client.download("demo/file.txt", "download.txt")
+
+    await client.list("demo")
+
+    await client.disconnect()
+
+
+asyncio.run(main())
diff --git a/micropython/bluetooth/aioble/examples/l2cap_file_server.py b/micropython/bluetooth/aioble/examples/l2cap_file_server.py
new file mode 100644
index 000000000..3c3b3b44d
--- /dev/null
+++ b/micropython/bluetooth/aioble/examples/l2cap_file_server.py
@@ -0,0 +1,183 @@
+# MIT license; Copyright (c) 2021 Jim Mussared
+
+# This is a BLE file server, based very loosely on the Object Transfer Service
+# specification. It demonstrated transfering data over an L2CAP channel, as
+# well as using notifications and GATT writes on a characteristic.
+
+# The server supports downloading and uploading files, as well as querying
+# directory listings and file sizes.
+
+# In order to access the file server, a client must connect, then establish an
+# L2CAP channel. To being an operation, a command is written to the control
+# characteristic, including a command number, sequence number, and filesystem
+# path. The response will be either via a notification on the control
+# characteristic (e.g. file size), or via the L2CAP channel (file contents or
+# directory listing).
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+
+import asyncio
+import aioble
+import bluetooth
+
+import struct
+import os
+
+# Randomly generated UUIDs.
+_FILE_SERVICE_UUID = bluetooth.UUID("0492fcec-7194-11eb-9439-0242ac130002")
+_CONTROL_CHARACTERISTIC_UUID = bluetooth.UUID("0492fcec-7194-11eb-9439-0242ac130003")
+
+# How frequently to send advertising beacons.
+_ADV_INTERVAL_MS = 250_000
+
+
+_COMMAND_SEND = const(0)
+_COMMAND_RECV = const(1)  # Not yet implemented.
+_COMMAND_LIST = const(2)
+_COMMAND_SIZE = const(3)
+_COMMAND_DONE = const(4)
+
+_STATUS_OK = const(0)
+_STATUS_NOT_IMPLEMENTED = const(1)
+_STATUS_NOT_FOUND = const(2)
+
+_L2CAP_PSN = const(22)
+_L2CAP_MTU = const(128)
+
+
+# Register GATT server.
+file_service = aioble.Service(_FILE_SERVICE_UUID)
+control_characteristic = aioble.Characteristic(
+    file_service, _CONTROL_CHARACTERISTIC_UUID, write=True, notify=True
+)
+aioble.register_services(file_service)
+
+
+send_file = None
+recv_file = None
+list_path = None
+op_seq = None
+l2cap_event = asyncio.Event()
+
+
+def send_done_notification(connection, status=_STATUS_OK):
+    global op_seq
+    control_characteristic.notify(connection, struct.pack("<BBB", _COMMAND_DONE, op_seq, status))
+    op_seq = None
+
+
+async def l2cap_task(connection):
+    global send_file, recv_file, list_path
+    try:
+        channel = await connection.l2cap_accept(_L2CAP_PSN, _L2CAP_MTU)
+        print("channel accepted")
+
+        while True:
+            await l2cap_event.wait()
+            l2cap_event.clear()
+
+            if send_file:
+                print("Sending:", send_file)
+                with open(send_file, "rb") as f:  # noqa: ASYNC230
+                    buf = bytearray(channel.peer_mtu)
+                    mv = memoryview(buf)
+                    while n := f.readinto(buf):
+                        await channel.send(mv[:n])
+                await channel.flush()
+                send_done_notification(connection)
+                send_file = None
+            if recv_file:
+                print("Receiving:", recv_file)
+                send_done_notification(connection, _STATUS_NOT_IMPLEMENTED)
+                recv_file = None
+            if list_path:
+                print("List:", list_path)
+                try:
+                    for name, _, _, size in os.ilistdir(list_path):
+                        await channel.send("{}:{}\n".format(size, name))
+                    await channel.send("\n")
+                    await channel.flush()
+                    send_done_notification(connection)
+                except OSError:
+                    send_done_notification(connection, _STATUS_NOT_FOUND)
+                list_path = None
+
+    except aioble.DeviceDisconnectedError:
+        print("Stopping l2cap")
+        return
+
+
+async def control_task(connection):
+    global send_file, recv_file, list_path
+
+    try:
+        with connection.timeout(None):
+            while True:
+                print("Waiting for write")
+                await control_characteristic.written()
+                msg = control_characteristic.read()
+                control_characteristic.write(b"")
+
+                if len(msg) < 3:
+                    continue
+
+                # Message is <command><seq><path...>.
+
+                command = msg[0]
+                seq = msg[1]
+                file = msg[2:].decode()
+
+                if command == _COMMAND_SEND:
+                    send_file = file
+                    l2cap_event.set()
+                elif command == _COMMAND_RECV:
+                    recv_file = file
+                    l2cap_event.set()
+                elif command == _COMMAND_LIST:
+                    list_path = file
+                    l2cap_event.set()
+                elif command == _COMMAND_SIZE:
+                    try:
+                        stat = os.stat(file)
+                        size = stat[6]
+                        status = 0
+                    except OSError:
+                        size = 0
+                        status = _STATUS_NOT_FOUND
+                    control_characteristic.notify(
+                        connection, struct.pack("<BBI", seq, status, size)
+                    )
+    except aioble.DeviceDisconnectedError:
+        return
+
+
+# Serially wait for connections. Don't advertise while a central is
+# connected.
+async def peripheral_task():
+    while True:
+        print("Waiting for connection")
+        connection = await aioble.advertise(
+            _ADV_INTERVAL_MS,
+            name="mpy-file",
+            services=[_FILE_SERVICE_UUID],
+        )
+        print("Connection from", connection.device)
+
+        t = asyncio.create_task(l2cap_task(connection))
+        await control_task(connection)
+        t.cancel()
+
+        await connection.disconnected()
+
+
+# Run both tasks.
+async def main():
+    await peripheral_task()
+
+
+asyncio.run(main())
diff --git a/micropython/bluetooth/aioble/examples/temp_client.py b/micropython/bluetooth/aioble/examples/temp_client.py
new file mode 100644
index 000000000..0840359f8
--- /dev/null
+++ b/micropython/bluetooth/aioble/examples/temp_client.py
@@ -0,0 +1,64 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+
+import asyncio
+import aioble
+import bluetooth
+
+import random
+import struct
+
+# org.bluetooth.service.environmental_sensing
+_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
+# org.bluetooth.characteristic.temperature
+_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)
+
+
+# Helper to decode the temperature characteristic encoding (sint16, hundredths of a degree).
+def _decode_temperature(data):
+    return struct.unpack("<h", data)[0] / 100
+
+
+async def find_temp_sensor():
+    # Scan for 5 seconds, in active mode, with very low interval/window (to
+    # maximise detection rate).
+    async with aioble.scan(5000, interval_us=30000, window_us=30000, active=True) as scanner:
+        async for result in scanner:
+            # See if it matches our name and the environmental sensing service.
+            if result.name() == "mpy-temp" and _ENV_SENSE_UUID in result.services():
+                return result.device
+    return None
+
+
+async def main():
+    device = await find_temp_sensor()
+    if not device:
+        print("Temperature sensor not found")
+        return
+
+    try:
+        print("Connecting to", device)
+        connection = await device.connect()
+    except asyncio.TimeoutError:
+        print("Timeout during connection")
+        return
+
+    async with connection:
+        try:
+            temp_service = await connection.service(_ENV_SENSE_UUID)
+            temp_characteristic = await temp_service.characteristic(_ENV_SENSE_TEMP_UUID)
+        except asyncio.TimeoutError:
+            print("Timeout discovering services/characteristics")
+            return
+
+        while connection.is_connected():
+            temp_deg_c = _decode_temperature(await temp_characteristic.read())
+            print("Temperature: {:.2f}".format(temp_deg_c))
+            await asyncio.sleep_ms(1000)
+
+
+asyncio.run(main())
diff --git a/micropython/bluetooth/aioble/examples/temp_sensor.py b/micropython/bluetooth/aioble/examples/temp_sensor.py
new file mode 100644
index 000000000..54580f595
--- /dev/null
+++ b/micropython/bluetooth/aioble/examples/temp_sensor.py
@@ -0,0 +1,69 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+
+import asyncio
+import aioble
+import bluetooth
+
+import random
+import struct
+
+# org.bluetooth.service.environmental_sensing
+_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
+# org.bluetooth.characteristic.temperature
+_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)
+# org.bluetooth.characteristic.gap.appearance.xml
+_ADV_APPEARANCE_GENERIC_THERMOMETER = const(768)
+
+# How frequently to send advertising beacons.
+_ADV_INTERVAL_MS = 250_000
+
+
+# Register GATT server.
+temp_service = aioble.Service(_ENV_SENSE_UUID)
+temp_characteristic = aioble.Characteristic(
+    temp_service, _ENV_SENSE_TEMP_UUID, read=True, notify=True
+)
+aioble.register_services(temp_service)
+
+
+# Helper to encode the temperature characteristic encoding (sint16, hundredths of a degree).
+def _encode_temperature(temp_deg_c):
+    return struct.pack("<h", int(temp_deg_c * 100))
+
+
+# This would be periodically polling a hardware sensor.
+async def sensor_task():
+    t = 24.5
+    while True:
+        temp_characteristic.write(_encode_temperature(t), send_update=True)
+        t += random.uniform(-0.5, 0.5)
+        await asyncio.sleep_ms(1000)
+
+
+# Serially wait for connections. Don't advertise while a central is
+# connected.
+async def peripheral_task():
+    while True:
+        async with await aioble.advertise(
+            _ADV_INTERVAL_MS,
+            name="mpy-temp",
+            services=[_ENV_SENSE_UUID],
+            appearance=_ADV_APPEARANCE_GENERIC_THERMOMETER,
+        ) as connection:
+            print("Connection from", connection.device)
+            await connection.disconnected(timeout_ms=None)
+
+
+# Run both tasks.
+async def main():
+    t1 = asyncio.create_task(sensor_task())
+    t2 = asyncio.create_task(peripheral_task())
+    await asyncio.gather(t1, t2)
+
+
+asyncio.run(main())
diff --git a/micropython/bluetooth/aioble/manifest.py b/micropython/bluetooth/aioble/manifest.py
new file mode 100644
index 000000000..1cf06c4d8
--- /dev/null
+++ b/micropython/bluetooth/aioble/manifest.py
@@ -0,0 +1,15 @@
+# This directory contains all aioble code, but the manifest itself just
+# forwards to the component manifests, which themselves reference the actual
+# code. This allows (for development purposes) all the files to live in the
+# one directory.
+
+metadata(version="0.6.1")
+
+# Default installation gives you everything. Install the individual
+# components (or a combination of them) if you want a more minimal install.
+require("aioble-peripheral")
+require("aioble-server")
+require("aioble-central")
+require("aioble-client")
+require("aioble-l2cap")
+require("aioble-security")
diff --git a/micropython/bluetooth/aioble/multitests/ble_advertise_services.py b/micropython/bluetooth/aioble/multitests/ble_advertise_services.py
new file mode 100644
index 000000000..d849c387f
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_advertise_services.py
@@ -0,0 +1,71 @@
+# Test advertising multiple services, and scanning them.
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+import asyncio
+import aioble
+import bluetooth
+
+TIMEOUT_MS = 5000
+
+_SERVICE_16_A = bluetooth.UUID(0x180F)  # Battery Service
+_SERVICE_16_B = bluetooth.UUID(0x181A)  # Environmental Sensing Service
+_SERVICE_32_A = bluetooth.UUID("AB12")  # random
+_SERVICE_32_B = bluetooth.UUID("CD34")  # random
+
+
+# Acting in peripheral role (advertising).
+async def instance0_task():
+    multitest.globals(BDADDR=aioble.config("mac"))
+    multitest.next()
+
+    # Advertise, and wait for central to connect to us.
+    print("advertise")
+    async with await aioble.advertise(
+        20_000,
+        name="MPY",
+        services=[_SERVICE_16_A, _SERVICE_16_B, _SERVICE_32_A, _SERVICE_32_B],
+        timeout_ms=TIMEOUT_MS,
+    ) as connection:
+        print("connected")
+        await connection.disconnected()
+        print("disconnected")
+
+
+def instance0():
+    try:
+        asyncio.run(instance0_task())
+    finally:
+        aioble.stop()
+
+
+# Acting in central role (scanning).
+async def instance1_task():
+    multitest.next()
+
+    wanted_device = aioble.Device(*BDADDR)
+
+    # Scan for the wanted device/peripheral and print its advertised services.
+    async with aioble.scan(5000, interval_us=30000, window_us=30000, active=True) as scanner:
+        async for result in scanner:
+            if result.device == wanted_device:
+                services = list(result.services())
+                if services:
+                    print(services)
+                    break
+
+    # Connect to peripheral and then disconnect.
+    print("connect")
+    device = aioble.Device(*BDADDR)
+    async with await device.connect(timeout_ms=TIMEOUT_MS):
+        print("disconnect")
+
+
+def instance1():
+    try:
+        asyncio.run(instance1_task())
+    finally:
+        aioble.stop()
diff --git a/micropython/bluetooth/aioble/multitests/ble_advertise_services.py.exp b/micropython/bluetooth/aioble/multitests/ble_advertise_services.py.exp
new file mode 100644
index 000000000..c0b2d974a
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_advertise_services.py.exp
@@ -0,0 +1,8 @@
+--- instance0 ---
+advertise
+connected
+disconnected
+--- instance1 ---
+[UUID(0x180f), UUID(0x181a), UUID(0x32314241), UUID(0x34334443)]
+connect
+disconnect
diff --git a/micropython/bluetooth/aioble/multitests/ble_buffered_characteristic.py b/micropython/bluetooth/aioble/multitests/ble_buffered_characteristic.py
new file mode 100644
index 000000000..e41c3fd1e
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_buffered_characteristic.py
@@ -0,0 +1,141 @@
+# Test characteristic read/write/notify from both GATTS and GATTC.
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+import machine
+import time
+
+import asyncio
+import aioble
+import bluetooth
+
+TIMEOUT_MS = 5000
+
+SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
+CHAR1_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
+CHAR2_UUID = bluetooth.UUID("00000000-1111-2222-3333-555555555555")
+CHAR3_UUID = bluetooth.UUID("00000000-1111-2222-3333-666666666666")
+
+
+# Acting in peripheral role.
+async def instance0_task():
+    service = aioble.Service(SERVICE_UUID)
+    characteristic1 = aioble.BufferedCharacteristic(service, CHAR1_UUID, write=True)
+    characteristic2 = aioble.BufferedCharacteristic(service, CHAR2_UUID, write=True, max_len=40)
+    characteristic3 = aioble.BufferedCharacteristic(
+        service, CHAR3_UUID, write=True, max_len=80, append=True
+    )
+    aioble.register_services(service)
+
+    multitest.globals(BDADDR=aioble.config("mac"))
+    multitest.next()
+
+    # Wait for central to connect to us.
+    print("advertise")
+    connection = await aioble.advertise(
+        20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
+    )
+    print("connected")
+
+    # The first will just see the second write (truncated).
+    await characteristic1.written(timeout_ms=TIMEOUT_MS)
+    await characteristic1.written(timeout_ms=TIMEOUT_MS)
+    print("written", characteristic1.read())
+
+    # The second will just see the second write (still truncated because MTU
+    # exchange hasn't happened).
+    await characteristic2.written(timeout_ms=TIMEOUT_MS)
+    await characteristic2.written(timeout_ms=TIMEOUT_MS)
+    print("written", characteristic2.read())
+
+    # MTU exchange should happen here.
+
+    # The second will now see the full second write.
+    await characteristic2.written(timeout_ms=TIMEOUT_MS)
+    await characteristic2.written(timeout_ms=TIMEOUT_MS)
+    print("written", characteristic2.read())
+
+    # The third will see the two full writes concatenated.
+    await characteristic3.written(timeout_ms=TIMEOUT_MS)
+    await characteristic3.written(timeout_ms=TIMEOUT_MS)
+    print("written", characteristic3.read())
+
+    # Wait for the central to disconnect.
+    await connection.disconnected(timeout_ms=TIMEOUT_MS)
+    print("disconnected")
+
+
+def instance0():
+    try:
+        asyncio.run(instance0_task())
+    finally:
+        aioble.stop()
+
+
+# Acting in central role.
+async def instance1_task():
+    multitest.next()
+
+    # Connect to peripheral and then disconnect.
+    print("connect")
+    device = aioble.Device(*BDADDR)
+    connection = await device.connect(timeout_ms=TIMEOUT_MS)
+
+    # Discover characteristics.
+    service = await connection.service(SERVICE_UUID)
+    print("service", service.uuid)
+    characteristic1 = await service.characteristic(CHAR1_UUID)
+    print("characteristic1", characteristic1.uuid)
+    characteristic2 = await service.characteristic(CHAR2_UUID)
+    print("characteristic2", characteristic2.uuid)
+    characteristic3 = await service.characteristic(CHAR3_UUID)
+    print("characteristic3", characteristic3.uuid)
+
+    # Write to each characteristic twice, with a long enough value to trigger
+    # truncation.
+    print("write1")
+    await characteristic1.write(
+        "central1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", response=True, timeout_ms=TIMEOUT_MS
+    )
+    await characteristic1.write(
+        "central1-bbbbbbbbbbbbbbbbbbbbbbbbbbbbb", response=True, timeout_ms=TIMEOUT_MS
+    )
+    print("write2a")
+    await characteristic2.write(
+        "central2a-aaaaaaaaaaaaaaaaaaaaaaaaaaaa", response=True, timeout_ms=TIMEOUT_MS
+    )
+    await characteristic2.write(
+        "central2a-bbbbbbbbbbbbbbbbbbbbbbbbbbbb", response=True, timeout_ms=TIMEOUT_MS
+    )
+    print("exchange mtu")
+    await connection.exchange_mtu(100)
+    print("write2b")
+    await characteristic2.write(
+        "central2b-aaaaaaaaaaaaaaaaaaaaaaaaaaaa", response=True, timeout_ms=TIMEOUT_MS
+    )
+    await characteristic2.write(
+        "central2b-bbbbbbbbbbbbbbbbbbbbbbbbbbbb", response=True, timeout_ms=TIMEOUT_MS
+    )
+    print("write3")
+    await characteristic3.write(
+        "central3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", response=True, timeout_ms=TIMEOUT_MS
+    )
+    await characteristic3.write(
+        "central3-bbbbbbbbbbbbbbbbbbbbbbbbbbbbb", response=True, timeout_ms=TIMEOUT_MS
+    )
+
+    # Disconnect from peripheral.
+    print("disconnect")
+    await connection.disconnect(timeout_ms=TIMEOUT_MS)
+    print("disconnected")
+
+
+def instance1():
+    try:
+        asyncio.run(instance1_task())
+    finally:
+        aioble.stop()
diff --git a/micropython/bluetooth/aioble/multitests/ble_buffered_characteristic.py.exp b/micropython/bluetooth/aioble/multitests/ble_buffered_characteristic.py.exp
new file mode 100644
index 000000000..3c00eacff
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_buffered_characteristic.py.exp
@@ -0,0 +1,21 @@
+--- instance0 ---
+advertise
+connected
+written b'central1-bbbbbbbbbbb'
+written b'central2a-bbbbbbbbbb'
+written b'central2b-bbbbbbbbbbbbbbbbbbbbbbbbbbbb'
+written b'central3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaacentral3-bbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
+disconnected
+--- instance1 ---
+connect
+service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
+characteristic1 UUID('00000000-1111-2222-3333-444444444444')
+characteristic2 UUID('00000000-1111-2222-3333-555555555555')
+characteristic3 UUID('00000000-1111-2222-3333-666666666666')
+write1
+write2a
+exchange mtu
+write2b
+write3
+disconnect
+disconnected
diff --git a/micropython/bluetooth/aioble/multitests/ble_characteristic.py b/micropython/bluetooth/aioble/multitests/ble_characteristic.py
new file mode 100644
index 000000000..0c42bc19b
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_characteristic.py
@@ -0,0 +1,138 @@
+# Test characteristic read/write/notify from both GATTS and GATTC.
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+import machine
+import time
+
+import asyncio
+import aioble
+import bluetooth
+
+TIMEOUT_MS = 5000
+
+SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
+CHAR_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
+
+
+# Acting in peripheral role.
+async def instance0_task():
+    service = aioble.Service(SERVICE_UUID)
+    characteristic = aioble.Characteristic(
+        service, CHAR_UUID, read=True, write=True, notify=True, indicate=True
+    )
+    aioble.register_services(service)
+
+    multitest.globals(BDADDR=aioble.config("mac"))
+    multitest.next()
+
+    # Write initial characteristic value.
+    characteristic.write("periph0")
+
+    # Wait for central to connect to us.
+    print("advertise")
+    connection = await aioble.advertise(
+        20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
+    )
+    print("connected")
+
+    # A
+
+    # Wait for a write to the characteristic from the central,
+    # then reply with a notification.
+    await characteristic.written(timeout_ms=TIMEOUT_MS)
+    print("written", characteristic.read())
+    print("write")
+    characteristic.write("periph1")
+    print("notify")
+    characteristic.notify(connection)
+
+    # B
+
+    # Wait for a write to the characteristic from the central,
+    # then reply with value-included notification.
+    await characteristic.written(timeout_ms=TIMEOUT_MS)
+    print("written", characteristic.read())
+    print("notify")
+    characteristic.notify(connection, "periph2")
+
+    # C
+
+    # Wait for a write to the characteristic from the central,
+    # then reply with an indication.
+    await characteristic.written(timeout_ms=TIMEOUT_MS)
+    print("written", characteristic.read())
+    print("write")
+    characteristic.write("periph3")
+    print("indicate")
+    await characteristic.indicate(connection, timeout_ms=TIMEOUT_MS)
+
+    # Wait for the central to disconnect.
+    await connection.disconnected(timeout_ms=TIMEOUT_MS)
+    print("disconnected")
+
+
+def instance0():
+    try:
+        asyncio.run(instance0_task())
+    finally:
+        aioble.stop()
+
+
+# Acting in central role.
+async def instance1_task():
+    multitest.next()
+
+    # Connect to peripheral and then disconnect.
+    print("connect")
+    device = aioble.Device(*BDADDR)
+    connection = await device.connect(timeout_ms=TIMEOUT_MS)
+
+    # Discover characteristics.
+    service = await connection.service(SERVICE_UUID)
+    print("service", service.uuid)
+    characteristic = await service.characteristic(CHAR_UUID)
+    print("characteristic", characteristic.uuid)
+
+    # Issue read of characteristic, should get initial value.
+    print("read", await characteristic.read(timeout_ms=TIMEOUT_MS))
+
+    # Write to the characteristic, which will trigger a notification.
+    print("write")
+    await characteristic.write("central0", response=True, timeout_ms=TIMEOUT_MS)
+    # A
+    print("notified", await characteristic.notified(timeout_ms=TIMEOUT_MS))
+    # Read the new value set immediately before notification.
+    print("read", await characteristic.read(timeout_ms=TIMEOUT_MS))
+
+    # Write to the characteristic, which will trigger a value-included notification.
+    print("write")
+    await characteristic.write("central1", response=True, timeout_ms=TIMEOUT_MS)
+    # B
+    print("notified", await characteristic.notified(timeout_ms=TIMEOUT_MS))
+    # Read value should be unchanged.
+    print("read", await characteristic.read(timeout_ms=TIMEOUT_MS))
+
+    # Write to the characteristic, which will trigger an indication.
+    print("write")
+    await characteristic.write("central2", response=True, timeout_ms=TIMEOUT_MS)
+    # C
+    print("indicated", await characteristic.indicated(timeout_ms=TIMEOUT_MS))
+    # Read the new value set immediately before indication.
+    print("read", await characteristic.read(timeout_ms=TIMEOUT_MS))
+
+    # Disconnect from peripheral.
+    print("disconnect")
+    await connection.disconnect(timeout_ms=TIMEOUT_MS)
+    print("disconnected")
+
+
+def instance1():
+    try:
+        asyncio.run(instance1_task())
+    finally:
+        aioble.stop()
diff --git a/micropython/bluetooth/aioble/multitests/ble_characteristic.py.exp b/micropython/bluetooth/aioble/multitests/ble_characteristic.py.exp
new file mode 100644
index 000000000..bc82cfa4f
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_characteristic.py.exp
@@ -0,0 +1,28 @@
+--- instance0 ---
+advertise
+connected
+written b'central0'
+write
+notify
+written b'central1'
+notify
+written b'central2'
+write
+indicate
+disconnected
+--- instance1 ---
+connect
+service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
+characteristic UUID('00000000-1111-2222-3333-444444444444')
+read b'periph0'
+write
+notified b'periph1'
+read b'periph1'
+write
+notified b'periph2'
+read b'central1'
+write
+indicated b'periph3'
+read b'periph3'
+disconnect
+disconnected
diff --git a/micropython/bluetooth/aioble/multitests/ble_descriptor.py b/micropython/bluetooth/aioble/multitests/ble_descriptor.py
new file mode 100644
index 000000000..8e32a469a
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_descriptor.py
@@ -0,0 +1,136 @@
+# Test descriptor discovery/read/write from both GATTS and GATTC.
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+import machine
+import time
+
+import asyncio
+import aioble
+import bluetooth
+
+TIMEOUT_MS = 5000
+
+SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
+CHAR1_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
+CHAR1_DESC1_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444445")
+CHAR1_DESC2_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444446")
+CHAR2_UUID = bluetooth.UUID("00000000-1111-2222-3333-555555555555")
+CHAR3_UUID = bluetooth.UUID("00000000-1111-2222-3333-666666666666")
+CHAR3_DESC1_UUID = bluetooth.UUID("00000000-1111-2222-3333-666666666667")
+
+
+# Acting in peripheral role.
+async def instance0_task():
+    service = aioble.Service(SERVICE_UUID)
+    char1 = aioble.Characteristic(
+        service, CHAR1_UUID, read=True, write=True, notify=True, indicate=True
+    )
+    char1_desc1 = aioble.Descriptor(char1, CHAR1_DESC1_UUID, read=True)
+    char1_desc1.write("char1_desc1")
+    char1_desc2 = aioble.Descriptor(char1, CHAR1_DESC2_UUID, read=True, write=True)
+    char1_desc2.write("char1_desc2")
+    aioble.Characteristic(service, CHAR2_UUID, read=True, write=True, notify=True, indicate=True)
+    char3 = aioble.Characteristic(
+        service, CHAR3_UUID, read=True, write=True, notify=True, indicate=True
+    )
+    char3_desc1 = aioble.Descriptor(char3, CHAR3_DESC1_UUID, read=True)
+    char3_desc1.write("char3_desc1")
+    aioble.register_services(service)
+
+    multitest.globals(BDADDR=aioble.config("mac"))
+    multitest.next()
+
+    # Wait for central to connect to us.
+    print("advertise")
+    connection = await aioble.advertise(
+        20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
+    )
+    print("connected")
+
+    await char1_desc2.written()
+    print("written", char1_desc2.read())
+
+    # Wait for the central to disconnect.
+    await connection.disconnected(timeout_ms=TIMEOUT_MS)
+    print("disconnected")
+
+
+def instance0():
+    try:
+        asyncio.run(instance0_task())
+    finally:
+        aioble.stop()
+
+
+# Acting in central role.
+async def instance1_task():
+    multitest.next()
+
+    # Connect to peripheral and then disconnect.
+    print("connect")
+    device = aioble.Device(*BDADDR)
+    connection = await device.connect(timeout_ms=TIMEOUT_MS)
+
+    service = await connection.service(SERVICE_UUID)
+    print("service", service.uuid)
+
+    # Discover characteristics.
+    uuids = []
+    async for char in service.characteristics():
+        uuids.append(char.uuid)
+    print("found", sorted(uuids))
+
+    char1 = await service.characteristic(CHAR1_UUID)
+    print("char1", char1.uuid)
+
+    uuids = []
+    async for desc in char1.descriptors():
+        uuids.append(desc.uuid)
+    print("char1 descs", sorted(uuids))
+
+    char1_desc1 = await char1.descriptor(CHAR1_DESC1_UUID)
+    print("char1 desc1", char1_desc1.uuid)
+    print("read", await char1_desc1.read())
+
+    char1_desc2 = await char1.descriptor(CHAR1_DESC2_UUID)
+    print("char1 desc2", char1_desc2.uuid)
+    print("read", await char1_desc2.read())
+    print("write")
+    await char1_desc2.write("update")
+
+    char2 = await service.characteristic(CHAR2_UUID)
+    print("char2", char2.uuid)
+
+    uuids = []
+    async for desc in char2.descriptors():
+        uuids.append(desc.uuid)
+    print("char2 descs", sorted(uuids))
+
+    char3 = await service.characteristic(CHAR3_UUID)
+    print("char3", char3.uuid)
+
+    uuids = []
+    async for desc in char3.descriptors():
+        uuids.append(desc.uuid)
+    print("char3 descs", sorted(uuids))
+
+    char3_desc1 = await char3.descriptor(CHAR3_DESC1_UUID)
+    print("char3 desc1", char3_desc1.uuid)
+    print("read", await char3_desc1.read())
+
+    # Disconnect from peripheral.
+    print("disconnect")
+    await connection.disconnect(timeout_ms=TIMEOUT_MS)
+    print("disconnected")
+
+
+def instance1():
+    try:
+        asyncio.run(instance1_task())
+    finally:
+        aioble.stop()
diff --git a/micropython/bluetooth/aioble/multitests/ble_descriptor.py.exp b/micropython/bluetooth/aioble/multitests/ble_descriptor.py.exp
new file mode 100644
index 000000000..44ed4797b
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_descriptor.py.exp
@@ -0,0 +1,24 @@
+--- instance0 ---
+advertise
+connected
+written b'update'
+disconnected
+--- instance1 ---
+connect
+service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
+found [UUID('00000000-1111-2222-3333-444444444444'), UUID('00000000-1111-2222-3333-555555555555'), UUID('00000000-1111-2222-3333-666666666666')]
+char1 UUID('00000000-1111-2222-3333-444444444444')
+char1 descs [UUID(0x2902), UUID('00000000-1111-2222-3333-444444444445'), UUID('00000000-1111-2222-3333-444444444446')]
+char1 desc1 UUID('00000000-1111-2222-3333-444444444445')
+read b'char1_desc1'
+char1 desc2 UUID('00000000-1111-2222-3333-444444444446')
+read b'char1_desc2'
+write
+char2 UUID('00000000-1111-2222-3333-555555555555')
+char2 descs [UUID(0x2902)]
+char3 UUID('00000000-1111-2222-3333-666666666666')
+char3 descs [UUID(0x2902), UUID('00000000-1111-2222-3333-666666666667')]
+char3 desc1 UUID('00000000-1111-2222-3333-666666666667')
+read b'char3_desc1'
+disconnect
+disconnected
diff --git a/micropython/bluetooth/aioble/multitests/ble_notify.py b/micropython/bluetooth/aioble/multitests/ble_notify.py
new file mode 100644
index 000000000..6eb85f68c
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_notify.py
@@ -0,0 +1,150 @@
+# Test notification-specific behavior.
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+import machine
+import time
+
+import asyncio
+import aioble
+import bluetooth
+
+TIMEOUT_MS = 5000
+
+SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
+CHAR_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
+
+
+# Acting in peripheral role.
+async def instance0_task():
+    service = aioble.Service(SERVICE_UUID)
+    characteristic = aioble.Characteristic(service, CHAR_UUID, read=True, notify=True)
+    aioble.register_services(service)
+
+    multitest.globals(BDADDR=aioble.config("mac"))
+    multitest.next()
+
+    # Wait for central to connect to us.
+    print("advertise")
+    connection = await aioble.advertise(
+        20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
+    )
+    print("connected")
+
+    # Send a subscribed-write (but client isn't subscribed, won't send anything).
+    multitest.wait("discovery")
+    await asyncio.sleep_ms(100)
+    characteristic.write("before-subscribe", send_update=True)
+
+    # Send a subscribed-write (now client is subscribed, client should get notified).
+    multitest.wait("subscribed")
+    await asyncio.sleep_ms(100)
+    characteristic.write("after-subscribe", send_update=True)
+
+    # Send a subscribed-write (now client is unsubscribed, won't send anything).
+    multitest.wait("unsubscribed")
+    await asyncio.sleep_ms(100)
+    characteristic.write("after-unsubscribe", send_update=True)
+
+    # Send 5 direct notifications.
+    multitest.wait("start-direct")
+    for i in range(5):
+        # Send 1 notification each time, except for 3 quick notifications the third time.
+        # The client should only see the last one.
+        for j in range(3 if i == 2 else 1):
+            if j > 0:
+                await asyncio.sleep_ms(100)
+            msg = "direct-{}-{}".format(i, j)
+            print("notify", msg)
+            characteristic.notify(connection, msg)
+
+        # Tell client to wait for notification.
+        multitest.broadcast("notified")
+        # Wait until client is ready for next notification.
+        multitest.wait("next")
+
+    # Wait for the central to disconnect.
+    await connection.disconnected(timeout_ms=TIMEOUT_MS)
+    print("disconnected")
+
+
+def instance0():
+    try:
+        asyncio.run(instance0_task())
+    finally:
+        aioble.stop()
+
+
+# Acting in central role.
+async def instance1_task():
+    multitest.next()
+
+    # Connect to peripheral and then disconnect.
+    print("connect")
+    device = aioble.Device(*BDADDR)
+    connection = await device.connect(timeout_ms=TIMEOUT_MS)
+
+    # Discover characteristics.
+    service = await connection.service(SERVICE_UUID)
+    print("service", service.uuid)
+    characteristic = await service.characteristic(CHAR_UUID)
+    print("characteristic", characteristic.uuid)
+
+    # Expect to not receive a notification (not subscribed).
+    multitest.broadcast("discovery")
+    try:
+        await characteristic.notified(timeout_ms=500)
+        print("fail")
+        return
+    except asyncio.TimeoutError:
+        print("no notification")
+
+    # Subscribe and expect a notification.
+    await characteristic.subscribe(notify=True)
+    multitest.broadcast("subscribed")
+    value = await characteristic.notified()
+    print("notified", value)
+
+    # Unsubscribe, and expect not to receive a notification.
+    await characteristic.subscribe(notify=False)
+    multitest.broadcast("unsubscribed")
+    try:
+        await characteristic.notified(timeout_ms=500)
+        print("fail")
+        return
+    except asyncio.TimeoutError:
+        print("no notification")
+
+    # Receive 5 notifications.
+    multitest.broadcast("start-direct")
+    for i in range(5):
+        multitest.wait("notified")
+        await asyncio.sleep_ms(200)
+        value = await characteristic.notified()
+        print("notified", value)
+
+        # Expect that after receiving a notification we don't get another one
+        # until we broadcast to the server.
+        try:
+            value = await characteristic.notified(timeout_ms=100)
+            print("unexpected notify", value)
+        except asyncio.TimeoutError:
+            pass
+
+        multitest.broadcast("next")
+
+    # Disconnect from peripheral.
+    print("disconnect")
+    await connection.disconnect(timeout_ms=TIMEOUT_MS)
+    print("disconnected")
+
+
+def instance1():
+    try:
+        asyncio.run(instance1_task())
+    finally:
+        aioble.stop()
diff --git a/micropython/bluetooth/aioble/multitests/ble_notify.py.exp b/micropython/bluetooth/aioble/multitests/ble_notify.py.exp
new file mode 100644
index 000000000..75901f045
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_notify.py.exp
@@ -0,0 +1,25 @@
+--- instance0 ---
+advertise
+connected
+notify direct-0-0
+notify direct-1-0
+notify direct-2-0
+notify direct-2-1
+notify direct-2-2
+notify direct-3-0
+notify direct-4-0
+disconnected
+--- instance1 ---
+connect
+service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
+characteristic UUID('00000000-1111-2222-3333-444444444444')
+no notification
+notified b'after-subscribe'
+no notification
+notified b'direct-0-0'
+notified b'direct-1-0'
+notified b'direct-2-2'
+notified b'direct-3-0'
+notified b'direct-4-0'
+disconnect
+disconnected
diff --git a/micropython/bluetooth/aioble/multitests/ble_shutdown.py b/micropython/bluetooth/aioble/multitests/ble_shutdown.py
new file mode 100644
index 000000000..28fc53536
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_shutdown.py
@@ -0,0 +1,130 @@
+# Test for shutting down and restarting the BLE stack.
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+import machine
+import time
+
+import asyncio
+import aioble
+import bluetooth
+
+TIMEOUT_MS = 5000
+
+SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
+CHAR_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
+
+_L2CAP_PSN = const(22)
+_L2CAP_MTU = const(128)
+
+
+# Acting in peripheral role.
+async def instance0_task():
+    multitest.globals(BDADDR=aioble.config("mac"))
+    multitest.next()
+
+    for i in range(3):
+        service = aioble.Service(SERVICE_UUID)
+        characteristic = aioble.Characteristic(service, CHAR_UUID, read=True)
+        aioble.register_services(service)
+
+        # Write initial characteristic value.
+        characteristic.write("periph{}".format(i))
+
+        multitest.broadcast("connect-{}".format(i))
+
+        # Wait for central to connect to us.
+        print("advertise")
+        connection = await aioble.advertise(
+            20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
+        )
+        print("connected")
+
+        multitest.broadcast("connected-{}".format(i))
+
+        for j in range(3):
+            channel = await connection.l2cap_accept(_L2CAP_PSN, _L2CAP_MTU)
+            print("channel accepted")
+
+            buf = bytearray(10)
+            n = await channel.recvinto(buf)
+            print("recv", n, buf[:n])
+
+            multitest.broadcast("recv-{}-{}".format(i, j))
+
+            await channel.disconnected(5000)
+            print("channel disconnected")
+
+        # Wait for the central to disconnect.
+        await connection.disconnected(timeout_ms=TIMEOUT_MS)
+        print("disconnected")
+
+        # Shutdown aioble + modbluetooth.
+        print("shutdown")
+        aioble.stop()
+
+
+def instance0():
+    try:
+        asyncio.run(instance0_task())
+    finally:
+        aioble.stop()
+
+
+# Acting in central role.
+async def instance1_task():
+    multitest.next()
+
+    for i in range(3):
+        multitest.wait("connect-{}".format(i))
+        # Connect to peripheral.
+        print("connect")
+        device = aioble.Device(*BDADDR)
+        connection = await device.connect(timeout_ms=TIMEOUT_MS)
+
+        multitest.wait("connected-{}".format(i))
+
+        # Discover characteristics.
+        service = await connection.service(SERVICE_UUID)
+        print("service", service.uuid)
+        characteristic = await service.characteristic(CHAR_UUID)
+        print("characteristic", characteristic.uuid)
+
+        # Issue read of characteristic, should get initial value.
+        print("read", await characteristic.read(timeout_ms=TIMEOUT_MS))
+
+        for j in range(3):
+            print("connecting channel")
+            channel = await connection.l2cap_connect(_L2CAP_PSN, _L2CAP_MTU)
+            print("channel connected")
+
+            await channel.send("l2cap-{}-{}".format(i, j))
+            await channel.flush()
+
+            multitest.wait("recv-{}-{}".format(i, j))
+
+            print("disconnecting channel")
+            await channel.disconnect()
+            print("channel disconnected")
+
+            await asyncio.sleep_ms(100)
+
+        # Disconnect from peripheral.
+        print("disconnect")
+        await connection.disconnect(timeout_ms=TIMEOUT_MS)
+        print("disconnected")
+
+        # Shutdown aioble.
+        print("shutdown")
+        aioble.stop()
+
+
+def instance1():
+    try:
+        asyncio.run(instance1_task())
+    finally:
+        aioble.stop()
diff --git a/micropython/bluetooth/aioble/multitests/ble_shutdown.py.exp b/micropython/bluetooth/aioble/multitests/ble_shutdown.py.exp
new file mode 100644
index 000000000..b431d4070
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_shutdown.py.exp
@@ -0,0 +1,98 @@
+--- instance0 ---
+advertise
+connected
+channel accepted
+recv 9 bytearray(b'l2cap-0-0')
+channel disconnected
+channel accepted
+recv 9 bytearray(b'l2cap-0-1')
+channel disconnected
+channel accepted
+recv 9 bytearray(b'l2cap-0-2')
+channel disconnected
+disconnected
+shutdown
+advertise
+connected
+channel accepted
+recv 9 bytearray(b'l2cap-1-0')
+channel disconnected
+channel accepted
+recv 9 bytearray(b'l2cap-1-1')
+channel disconnected
+channel accepted
+recv 9 bytearray(b'l2cap-1-2')
+channel disconnected
+disconnected
+shutdown
+advertise
+connected
+channel accepted
+recv 9 bytearray(b'l2cap-2-0')
+channel disconnected
+channel accepted
+recv 9 bytearray(b'l2cap-2-1')
+channel disconnected
+channel accepted
+recv 9 bytearray(b'l2cap-2-2')
+channel disconnected
+disconnected
+shutdown
+--- instance1 ---
+connect
+service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
+characteristic UUID('00000000-1111-2222-3333-444444444444')
+read b'periph0'
+connecting channel
+channel connected
+disconnecting channel
+channel disconnected
+connecting channel
+channel connected
+disconnecting channel
+channel disconnected
+connecting channel
+channel connected
+disconnecting channel
+channel disconnected
+disconnect
+disconnected
+shutdown
+connect
+service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
+characteristic UUID('00000000-1111-2222-3333-444444444444')
+read b'periph1'
+connecting channel
+channel connected
+disconnecting channel
+channel disconnected
+connecting channel
+channel connected
+disconnecting channel
+channel disconnected
+connecting channel
+channel connected
+disconnecting channel
+channel disconnected
+disconnect
+disconnected
+shutdown
+connect
+service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
+characteristic UUID('00000000-1111-2222-3333-444444444444')
+read b'periph2'
+connecting channel
+channel connected
+disconnecting channel
+channel disconnected
+connecting channel
+channel connected
+disconnecting channel
+channel disconnected
+connecting channel
+channel connected
+disconnecting channel
+channel disconnected
+disconnect
+disconnected
+shutdown
diff --git a/micropython/bluetooth/aioble/multitests/ble_write_capture.py b/micropython/bluetooth/aioble/multitests/ble_write_capture.py
new file mode 100644
index 000000000..0577229e2
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_write_capture.py
@@ -0,0 +1,104 @@
+# Test characteristic write capture.
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+import machine
+import time
+
+import asyncio
+import aioble
+import bluetooth
+
+TIMEOUT_MS = 5000
+
+SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
+CHAR_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
+CHAR_CAPTURE_UUID = bluetooth.UUID("00000000-1111-2222-3333-555555555555")
+
+
+# Acting in peripheral role.
+async def instance0_task():
+    service = aioble.Service(SERVICE_UUID)
+    characteristic = aioble.Characteristic(
+        service,
+        CHAR_UUID,
+        write=True,
+    )
+    # Second characteristic enabled write capture.
+    characteristic_capture = aioble.Characteristic(
+        service,
+        CHAR_CAPTURE_UUID,
+        write=True,
+        capture=True,
+    )
+    aioble.register_services(service)
+
+    multitest.globals(BDADDR=aioble.config("mac"))
+    multitest.next()
+
+    # Wait for central to connect to us.
+    print("advertise")
+    async with await aioble.advertise(
+        20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
+    ) as connection:
+        print("connected")
+
+        # We should miss writes while we're sleeping.
+        for i in range(2):
+            await characteristic.written(timeout_ms=TIMEOUT_MS)
+            print("written", characteristic.read())
+            await asyncio.sleep_ms(500)
+
+        # Shouldn't miss any writes as they will be captured and queued.
+        for i in range(5):
+            write_connection, value = await characteristic_capture.written(timeout_ms=TIMEOUT_MS)
+            print("written", value, write_connection == connection)
+            await asyncio.sleep_ms(500)
+
+
+def instance0():
+    try:
+        asyncio.run(instance0_task())
+    finally:
+        aioble.stop()
+
+
+# Acting in central role.
+async def instance1_task():
+    multitest.next()
+
+    # Connect to peripheral and then disconnect.
+    print("connect")
+    device = aioble.Device(*BDADDR)
+    async with await device.connect(timeout_ms=TIMEOUT_MS) as connection:
+        # Discover characteristics.
+        service = await connection.service(SERVICE_UUID)
+        print("service", service.uuid)
+        characteristic = await service.characteristic(CHAR_UUID)
+        characteristic_capture = await service.characteristic(CHAR_CAPTURE_UUID)
+        print("characteristic", characteristic.uuid, characteristic_capture.uuid)
+
+        # Write to the characteristic five times, but faster than the remote side is waiting.
+        # Some writes will be lost.
+        for i in range(5):
+            print("write")
+            await characteristic.write("central" + str(i), timeout_ms=TIMEOUT_MS)
+            await asyncio.sleep_ms(200)
+
+        # Write to the capture characteristic five times, but faster than the remote side is waiting.
+        # The writes should be captured and queued.
+        for i in range(5):
+            print("write")
+            await characteristic_capture.write("central" + str(i), timeout_ms=TIMEOUT_MS)
+            await asyncio.sleep_ms(200)
+
+
+def instance1():
+    try:
+        asyncio.run(instance1_task())
+    finally:
+        aioble.stop()
diff --git a/micropython/bluetooth/aioble/multitests/ble_write_capture.py.exp b/micropython/bluetooth/aioble/multitests/ble_write_capture.py.exp
new file mode 100644
index 000000000..366af008b
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_write_capture.py.exp
@@ -0,0 +1,24 @@
+--- instance0 ---
+advertise
+connected
+written b'central0'
+written b'central1'
+written b'central0' True
+written b'central1' True
+written b'central2' True
+written b'central3' True
+written b'central4' True
+--- instance1 ---
+connect
+service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
+characteristic UUID('00000000-1111-2222-3333-444444444444') UUID('00000000-1111-2222-3333-555555555555')
+write
+write
+write
+write
+write
+write
+write
+write
+write
+write
diff --git a/micropython/bluetooth/aioble/multitests/ble_write_order.py b/micropython/bluetooth/aioble/multitests/ble_write_order.py
new file mode 100644
index 000000000..24da54c2d
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_write_order.py
@@ -0,0 +1,125 @@
+# Test characteristic write capture preserves order across characteristics.
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+import machine
+import time
+
+import asyncio
+import aioble
+import bluetooth
+
+TIMEOUT_MS = 5000
+
+# Without the write ordering (via the shared queue) in server.py, this test
+# passes with delay of 1, fails some at 5, fails more at 50
+DUMMY_DELAY = 50
+
+SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
+CHAR_FIRST_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
+CHAR_SECOND_UUID = bluetooth.UUID("00000000-1111-2222-3333-555555555555")
+
+
+# Acting in peripheral role.
+async def instance0_task():
+    service = aioble.Service(SERVICE_UUID)
+    characteristic_first = aioble.Characteristic(
+        service,
+        CHAR_FIRST_UUID,
+        write=True,
+        capture=True,
+    )
+    # Second characteristic enabled write capture.
+    characteristic_second = aioble.Characteristic(
+        service,
+        CHAR_SECOND_UUID,
+        write=True,
+        capture=True,
+    )
+    aioble.register_services(service)
+
+    # Register characteristic.written() handlers as asyncio background tasks.
+    # The order of these is important!
+    task_second = asyncio.create_task(task_written(characteristic_second, "second"))
+    task_first = asyncio.create_task(task_written(characteristic_first, "first"))
+
+    # This dummy task simulates background processing on a real system that
+    # can block the asyncio loop for brief periods of time
+    task_dummy_ = asyncio.create_task(task_dummy())
+
+    multitest.globals(BDADDR=aioble.config("mac"))
+    multitest.next()
+
+    # Wait for central to connect to us.
+    print("advertise")
+    async with await aioble.advertise(
+        20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
+    ) as connection:
+        print("connected")
+
+        await connection.disconnected()
+
+    task_second.cancel()
+    task_first.cancel()
+    task_dummy_.cancel()
+
+
+async def task_written(chr, label):
+    while True:
+        await chr.written()
+        data = chr.read().decode()
+        print(f"written: {label} {data}")
+
+
+async def task_dummy():
+    while True:
+        time.sleep_ms(DUMMY_DELAY)
+        await asyncio.sleep_ms(5)
+
+
+def instance0():
+    try:
+        asyncio.run(instance0_task())
+    finally:
+        aioble.stop()
+
+
+# Acting in central role.
+async def instance1_task():
+    multitest.next()
+
+    # Connect to peripheral and then disconnect.
+    print("connect")
+    device = aioble.Device(*BDADDR)
+    async with await device.connect(timeout_ms=TIMEOUT_MS) as connection:
+        # Discover characteristics.
+        service = await connection.service(SERVICE_UUID)
+        print("service", service.uuid)
+        characteristic_first = await service.characteristic(CHAR_FIRST_UUID)
+        characteristic_second = await service.characteristic(CHAR_SECOND_UUID)
+        print("characteristic", characteristic_first.uuid, characteristic_second.uuid)
+
+        for i in range(5):
+            print(f"write c{i}")
+            await characteristic_first.write("c" + str(i), timeout_ms=TIMEOUT_MS)
+            await characteristic_second.write("c" + str(i), timeout_ms=TIMEOUT_MS)
+
+            await asyncio.sleep_ms(300)
+
+        for i in range(5):
+            print(f"write r{i}")
+            await characteristic_second.write("r" + str(i), timeout_ms=TIMEOUT_MS)
+            await characteristic_first.write("r" + str(i), timeout_ms=TIMEOUT_MS)
+
+            await asyncio.sleep_ms(300)
+
+
+def instance1():
+    try:
+        asyncio.run(instance1_task())
+    finally:
+        aioble.stop()
diff --git a/micropython/bluetooth/aioble/multitests/ble_write_order.py.exp b/micropython/bluetooth/aioble/multitests/ble_write_order.py.exp
new file mode 100644
index 000000000..516de685c
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/ble_write_order.py.exp
@@ -0,0 +1,37 @@
+--- instance0 ---
+advertise
+connected
+written: first c0
+written: second c0
+written: first c1
+written: second c1
+written: first c2
+written: second c2
+written: first c3
+written: second c3
+written: first c4
+written: second c4
+written: second r0
+written: first r0
+written: second r1
+written: first r1
+written: second r2
+written: first r2
+written: second r3
+written: first r3
+written: second r4
+written: first r4
+--- instance1 ---
+connect
+service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
+characteristic UUID('00000000-1111-2222-3333-444444444444') UUID('00000000-1111-2222-3333-555555555555')
+write c0
+write c1
+write c2
+write c3
+write c4
+write r0
+write r1
+write r2
+write r3
+write r4
diff --git a/micropython/bluetooth/aioble/multitests/perf_gatt_notify.py b/micropython/bluetooth/aioble/multitests/perf_gatt_notify.py
new file mode 100644
index 000000000..d8a0ea173
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/perf_gatt_notify.py
@@ -0,0 +1,109 @@
+# Ping-pong GATT notifications between two devices.
+
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+import machine
+import time
+
+import asyncio
+import aioble
+import bluetooth
+
+TIMEOUT_MS = 5000
+
+SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
+CHAR_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
+
+# How long to run the test for.
+_NUM_NOTIFICATIONS = const(50)
+
+
+def register_server():
+    server_service = aioble.Service(SERVICE_UUID)
+    server_characteristic = aioble.Characteristic(
+        server_service, CHAR_UUID, read=True, write=True, notify=True, indicate=True
+    )
+    aioble.register_services(server_service)
+    return server_characteristic
+
+
+async def discover_server(connection):
+    client_service = await connection.service(SERVICE_UUID)
+    return await client_service.characteristic(CHAR_UUID)
+
+
+# Acting in peripheral role.
+async def instance0_task():
+    server_characteristic = register_server()
+
+    multitest.globals(BDADDR=aioble.config("mac"))
+    multitest.next()
+
+    connection = await aioble.advertise(
+        20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
+    )
+
+    print("connect")
+
+    client_characteristic = await discover_server(connection)
+
+    # Give the central enough time to discover chars.
+    await asyncio.sleep_ms(500)
+
+    ticks_start = time.ticks_ms()
+
+    for i in range(_NUM_NOTIFICATIONS):
+        # Send a notification and wait for a response.
+        server_characteristic.notify(connection, "peripheral" + str(i))
+        await client_characteristic.notified()
+
+    ticks_end = time.ticks_ms()
+    ticks_total = time.ticks_diff(ticks_end, ticks_start)
+    multitest.output_metric(
+        "Acknowledged {} notifications in {} ms. {} ms/notification.".format(
+            _NUM_NOTIFICATIONS, ticks_total, ticks_total // _NUM_NOTIFICATIONS
+        )
+    )
+
+    # Disconnect the central.
+    await connection.disconnect()
+
+
+def instance0():
+    try:
+        asyncio.run(instance0_task())
+    finally:
+        aioble.stop()
+
+
+# Acting in central role.
+async def instance1_task():
+    server_characteristic = register_server()
+
+    multitest.next()
+
+    device = aioble.Device(*BDADDR)
+    connection = await device.connect(timeout_ms=TIMEOUT_MS)
+
+    print("connect")
+
+    client_characteristic = await discover_server(connection)
+
+    for i in range(_NUM_NOTIFICATIONS):
+        # Wait for notification and send response.
+        data = await client_characteristic.notified()
+        server_characteristic.notify(connection, b"central" + data)
+
+    # Wait for the peripheral to disconnect us.
+    await connection.disconnected(timeout_ms=20000)
+
+
+def instance1():
+    try:
+        asyncio.run(instance1_task())
+    finally:
+        aioble.stop()
diff --git a/micropython/bluetooth/aioble/multitests/perf_gatt_notify.py.exp b/micropython/bluetooth/aioble/multitests/perf_gatt_notify.py.exp
new file mode 100644
index 000000000..4b7d220a0
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/perf_gatt_notify.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+connect
+--- instance1 ---
+connect
diff --git a/micropython/bluetooth/aioble/multitests/perf_l2cap.py b/micropython/bluetooth/aioble/multitests/perf_l2cap.py
new file mode 100644
index 000000000..05fd4863e
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/perf_l2cap.py
@@ -0,0 +1,111 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.append("")
+
+from micropython import const
+import machine
+import time
+
+import asyncio
+import aioble
+import bluetooth
+import random
+
+TIMEOUT_MS = 5000
+
+_L2CAP_PSM = const(22)
+_L2CAP_MTU = const(512)
+
+_PAYLOAD_LEN = const(_L2CAP_MTU)
+_NUM_PAYLOADS = const(20)
+
+_RANDOM_SEED = 22
+
+
+# Acting in peripheral role.
+async def instance0_task():
+    multitest.globals(BDADDR=aioble.config("mac"))
+    multitest.next()
+
+    connection = await aioble.advertise(
+        20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
+    )
+
+    print("connect")
+
+    channel = await connection.l2cap_accept(_L2CAP_PSM, _L2CAP_MTU, timeout_ms=TIMEOUT_MS)
+
+    random.seed(_RANDOM_SEED)
+
+    buf = bytearray(_PAYLOAD_LEN)
+
+    for i in range(_NUM_PAYLOADS):
+        for j in range(_PAYLOAD_LEN):
+            buf[j] = random.randint(0, 255)
+        await channel.send(buf)
+    await channel.flush()
+
+    await asyncio.sleep_ms(500)
+
+    await channel.disconnect()
+
+    # Disconnect the central.
+    await connection.disconnect()
+
+
+def instance0():
+    try:
+        asyncio.run(instance0_task())
+    finally:
+        aioble.stop()
+
+
+# Acting in central role.
+async def instance1_task():
+    multitest.next()
+
+    device = aioble.Device(*BDADDR)
+    connection = await device.connect(timeout_ms=TIMEOUT_MS)
+
+    print("connect")
+
+    await asyncio.sleep_ms(500)
+
+    channel = await connection.l2cap_connect(_L2CAP_PSM, _L2CAP_MTU, timeout_ms=TIMEOUT_MS)
+
+    random.seed(_RANDOM_SEED)
+
+    buf = bytearray(_PAYLOAD_LEN)
+
+    recv_bytes, recv_correct = 0, 0
+    expected_bytes = _PAYLOAD_LEN * _NUM_PAYLOADS
+
+    ticks_first_byte = 0
+    while recv_bytes < expected_bytes:
+        n = await channel.recvinto(buf)
+        if not ticks_first_byte:
+            ticks_first_byte = time.ticks_ms()
+        recv_bytes += n
+        for i in range(n):
+            if buf[i] == random.randint(0, 255):
+                recv_correct += 1
+
+    ticks_end = time.ticks_ms()
+    total_ticks = time.ticks_diff(ticks_end, ticks_first_byte)
+
+    multitest.output_metric(
+        "Received {}/{} bytes in {} ms. {} B/s".format(
+            recv_bytes, recv_correct, total_ticks, recv_bytes * 1000 // total_ticks
+        )
+    )
+
+    # Wait for the peripheral to disconnect us.
+    await connection.disconnected(timeout_ms=20000)
+
+
+def instance1():
+    try:
+        asyncio.run(instance1_task())
+    finally:
+        aioble.stop()
diff --git a/micropython/bluetooth/aioble/multitests/perf_l2cap.py.exp b/micropython/bluetooth/aioble/multitests/perf_l2cap.py.exp
new file mode 100644
index 000000000..4b7d220a0
--- /dev/null
+++ b/micropython/bluetooth/aioble/multitests/perf_l2cap.py.exp
@@ -0,0 +1,4 @@
+--- instance0 ---
+connect
+--- instance1 ---
+connect
diff --git a/micropython/bundles/README.md b/micropython/bundles/README.md
new file mode 100644
index 000000000..c4b3d19b0
--- /dev/null
+++ b/micropython/bundles/README.md
@@ -0,0 +1,7 @@
+These are "meta packages" designed to make it easy to provide defined
+bundles of related packages.
+
+For example, all deployments of MicroPython with networking support
+(WiFi/Ethernet) should add `require("bundle-networking")` to their
+`manifest.py` to ensure that the the standard set of networking packages
+(including HTTP requests, WebREPL, NTP, package management) are included.
diff --git a/micropython/bundles/bundle-networking/manifest.py b/micropython/bundles/bundle-networking/manifest.py
new file mode 100644
index 000000000..7ad3540da
--- /dev/null
+++ b/micropython/bundles/bundle-networking/manifest.py
@@ -0,0 +1,14 @@
+metadata(
+    version="0.2.0",
+    description="Common networking packages for all network-capable deployments of MicroPython.",
+)
+
+require("mip")
+require("ntptime")
+require("ssl")
+require("requests")
+require("webrepl")
+
+# Provide urequests (which just forwards to requests) for backwards
+# compatibility.
+require("urequests")
diff --git a/micropython/drivers/bus/onewire/manifest.py b/micropython/drivers/bus/onewire/manifest.py
new file mode 100644
index 000000000..32e2b57d6
--- /dev/null
+++ b/micropython/drivers/bus/onewire/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="Onewire driver.", version="0.1.0")
+
+module("onewire.py", opt=3)
diff --git a/micropython/drivers/bus/onewire/onewire.py b/micropython/drivers/bus/onewire/onewire.py
new file mode 100644
index 000000000..4c6da741c
--- /dev/null
+++ b/micropython/drivers/bus/onewire/onewire.py
@@ -0,0 +1,92 @@
+# 1-Wire driver for MicroPython
+# MIT license; Copyright (c) 2016 Damien P. George
+
+import _onewire as _ow
+
+
+class OneWireError(Exception):
+    pass
+
+
+class OneWire:
+    SEARCH_ROM = 0xF0
+    MATCH_ROM = 0x55
+    SKIP_ROM = 0xCC
+
+    def __init__(self, pin):
+        self.pin = pin
+        self.pin.init(pin.OPEN_DRAIN, pin.PULL_UP)
+
+    def reset(self, required=False):
+        reset = _ow.reset(self.pin)
+        if required and not reset:
+            raise OneWireError
+        return reset
+
+    def readbit(self):
+        return _ow.readbit(self.pin)
+
+    def readbyte(self):
+        return _ow.readbyte(self.pin)
+
+    def readinto(self, buf):
+        for i in range(len(buf)):
+            buf[i] = _ow.readbyte(self.pin)
+
+    def writebit(self, value):
+        return _ow.writebit(self.pin, value)
+
+    def writebyte(self, value):
+        return _ow.writebyte(self.pin, value)
+
+    def write(self, buf):
+        for b in buf:
+            _ow.writebyte(self.pin, b)
+
+    def select_rom(self, rom):
+        self.reset()
+        self.writebyte(self.MATCH_ROM)
+        self.write(rom)
+
+    def scan(self):
+        devices = []
+        diff = 65
+        rom = False
+        for i in range(0xFF):
+            rom, diff = self._search_rom(rom, diff)
+            if rom:
+                devices += [rom]
+            if diff == 0:
+                break
+        return devices
+
+    def _search_rom(self, l_rom, diff):
+        if not self.reset():
+            return None, 0
+        self.writebyte(self.SEARCH_ROM)
+        if not l_rom:
+            l_rom = bytearray(8)
+        rom = bytearray(8)
+        next_diff = 0
+        i = 64
+        for byte in range(8):
+            r_b = 0
+            for bit in range(8):
+                b = self.readbit()
+                if self.readbit():
+                    if b:  # there are no devices or there is an error on the bus
+                        return None, 0
+                else:
+                    if not b:  # collision, two devices with different bit meaning
+                        if diff > i or ((l_rom[byte] & (1 << bit)) and diff != i):
+                            b = 1
+                            next_diff = i
+                self.writebit(b)
+                if b:
+                    r_b |= 1 << bit
+                i -= 1
+            rom[byte] = r_b
+        return rom, next_diff
+
+    def crc8(self, data):
+        return _ow.crc8(data)
diff --git a/micropython/drivers/codec/wm8960/manifest.py b/micropython/drivers/codec/wm8960/manifest.py
new file mode 100644
index 000000000..3c8922645
--- /dev/null
+++ b/micropython/drivers/codec/wm8960/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="WM8960 codec.", version="0.1.1")
+
+module("wm8960.py", opt=3)
diff --git a/micropython/drivers/codec/wm8960/wm8960.py b/micropython/drivers/codec/wm8960/wm8960.py
new file mode 100644
index 000000000..313649f36
--- /dev/null
+++ b/micropython/drivers/codec/wm8960/wm8960.py
@@ -0,0 +1,737 @@
+#
+# Driver class for the WM8960 Codec to be used e.g. with MIMXRT_1xxx Boards.
+# Derived from the NXP SDK drivers.
+#
+# Copyright (c) 2015, Freescale Semiconductor, Inc., (C-Code)
+# Copyright 2016-2021 NXP, (C-Code)
+# All rights reserved.
+#
+# Translated to MicroPython by Robert Hammelrath, 2022
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+import array
+from micropython import const
+
+# Define the register addresses of WM8960.
+_LINVOL = const(0x0)
+_RINVOL = const(0x1)
+_LOUT1 = const(0x2)
+_ROUT1 = const(0x3)
+_CLOCK1 = const(0x4)
+_DACCTL1 = const(0x5)
+_DACCTL2 = const(0x6)
+_IFACE1 = const(0x7)
+_CLOCK2 = const(0x8)
+_IFACE2 = const(0x9)
+_LDAC = const(0xA)
+_RDAC = const(0xB)
+_RESET = const(0xF)
+_3D = const(0x10)
+_ALC1 = const(0x11)
+_ALC2 = const(0x12)
+_ALC3 = const(0x13)
+_NOISEG = const(0x14)
+_LADC = const(0x15)
+_RADC = const(0x16)
+_ADDCTL1 = const(0x17)
+# Register _ADDCTL2 = const(0x18)
+_POWER1 = const(0x19)
+_POWER2 = const(0x1A)
+_ADDCTL3 = const(0x1B)
+# Register _APOP1 = const(0x1C)
+# Register _APOP2 = const(0x1D)
+_LINPATH = const(0x20)
+_RINPATH = const(0x21)
+_LOUTMIX = const(0x22)
+_ROUTMIX = const(0x25)
+_MONOMIX1 = const(0x26)
+_MONOMIX2 = const(0x27)
+_LOUT2 = const(0x28)
+_ROUT2 = const(0x29)
+_MONO = const(0x2A)
+_INBMIX1 = const(0x2B)
+_INBMIX2 = const(0x2C)
+_BYPASS1 = const(0x2D)
+_BYPASS2 = const(0x2E)
+_POWER3 = const(0x2F)
+_ADDCTL4 = const(0x30)
+_CLASSD1 = const(0x31)
+# Register _CLASSD3 = const(0x33)
+_PLL1 = const(0x34)
+_PLL2 = const(0x35)
+_PLL3 = const(0x36)
+_PLL4 = const(0x37)
+
+# WM8960 PLLN range */
+_PLL_N_MIN_VALUE = const(6)
+_PLL_N_MAX_VALUE = const(12)
+
+# WM8960 CLOCK2 bits
+_CLOCK2_BCLK_DIV_MASK = const(0x0F)
+_CLOCK2_DCLK_DIV_MASK = const(0x1C0)
+_CLOCK2_DCLK_DIV_SHIFT = const(0x06)
+
+# Register _IFACE1
+_IFACE1_FORMAT_MASK = const(0x03)
+_IFACE1_WL_MASK = const(0x0C)
+_IFACE1_WL_SHIFT = const(0x02)
+_IFACE1_LRP_MASK = const(0x10)
+_IFACE1_MS_MASK = const(0x40)
+_IFACE1_DLRSWAP_MASK = const(0x20)
+_IFACE1_ALRSWAP_MASK = const(0x100)
+
+# Register _POWER1
+_POWER1_VREF_MASK = const(0x40)
+_POWER1_VREF_SHIFT = const(0x06)
+_POWER1_AINL_MASK = const(0x20)
+_POWER1_AINR_MASK = const(0x10)
+_POWER1_ADCL_MASK = const(0x08)
+_POWER1_ADCR_MASK = const(0x0)
+_POWER1_MICB_MASK = const(0x02)
+_POWER1_MICB_SHIFT = const(0x01)
+
+# Register _POWER2
+_POWER2_DACL_MASK = const(0x100)
+_POWER2_DACR_MASK = const(0x80)
+_POWER2_LOUT1_MASK = const(0x40)
+_POWER2_ROUT1_MASK = const(0x20)
+_POWER2_SPKL_MASK = const(0x10)
+_POWER2_SPKR_MASK = const(0x08)
+_POWER3_LMIC_MASK = const(0x20)
+_POWER3_RMIC_MASK = const(0x10)
+_POWER3_LOMIX_MASK = const(0x08)
+_POWER3_ROMIX_MASK = const(0x04)
+
+# Register _DACCTL1 .. 3
+_DACCTL1_MONOMIX_MASK = const(0x10)
+_DACCTL1_MONOMIX_SHIFT = const(0x4)
+_DACCTL1_DACMU_MASK = const(0x08)
+_DACCTL1_DEEM_MASK = const(0x06)
+_DACCTL1_DEEM_SHIFT = const(0x01)
+_DACCTL2_DACSMM_MASK = const(0x08)
+_DACCTL2_DACMR_MASK = const(0x04)
+_DACCTL3_ALCSR_MASK = const(0x07)
+
+# _WM8060_ALC1 .. 3
+_ALC_CHANNEL_MASK = const(0x180)
+_ALC_CHANNEL_SHIFT = const(0x7)
+_ALC_MODE_MASK = const(0x100)
+_ALC_MODE_SHIFT = const(0x8)
+_ALC_GAIN_MASK = const(0x70)
+_ALC_GAIN_SHIFT = const(0x4)
+_ALC_TARGET_MASK = const(0x0F)
+_ALC_ATTACK_MASK = const(0x0F)
+_ALC_DECAY_MASK = const(0xF0)
+_ALC_DECAY_SHIFT = const(4)
+_ALC_HOLD_MASK = const(0xF)
+
+# Register _NOISEG
+_NOISEG_LEVEL_SHIFT = const(3)
+
+_I2C_ADDR = const(0x1A)
+
+# WM8960 maximum volume values
+_MAX_VOLUME_ADC = const(0xFF)
+_MAX_VOLUME_DAC = const(0xFF)
+_MAX_VOLUME_HEADPHONE = const(0x7F)
+_MAX_VOLUME_LINEIN = const(0x3F)
+_MAX_VOLUME_SPEAKER = const(0x7F)
+
+# Config symbol names
+# Modules
+MODULE_ADC = const(0)  # ADC module in WM8960
+MODULE_DAC = const(1)  # DAC module in WM8960
+MODULE_VREF = const(2)  # VREF module
+MODULE_HEADPHONE = const(3)  # Headphone
+MODULE_MIC_BIAS = const(4)  # Mic bias
+MODULE_MIC = const(5)  # Input Mic
+MODULE_LINE_IN = const(6)  # Analog in PGA
+MODULE_LINE_OUT = const(7)  # Line out module
+MODULE_SPEAKER = const(8)  # Speaker module
+MODULE_OMIX = const(9)  # Output mixer
+MODULE_MONO_OUT = const(10)  # Mono mix
+
+# Route
+ROUTE_BYPASS = const(0)  # LINEIN->Headphone.
+ROUTE_PLAYBACK = const(1)  #  I2SIN->DAC->Headphone.
+ROUTE_PLAYBACK_RECORD = const(2)  # I2SIN->DAC->Headphone, LINEIN->ADC->I2SOUT.
+ROUTE_RECORD = const(5)  # LINEIN->ADC->I2SOUT.
+
+# Input
+INPUT_CLOSED = const(0)  # Input device is closed
+INPUT_MIC1 = const(1)  # Input as single ended mic, only use L/RINPUT1
+INPUT_MIC2 = const(2)  # Input as diff. mic, use L/RINPUT1 and L/RINPUT2
+INPUT_MIC3 = const(3)  # Input as diff. mic, use L/RINPUT1 and L/RINPUT3
+INPUT_LINE2 = const(4)  # Input as line input, only use L/RINPUT2
+INPUT_LINE3 = const(5)  # Input as line input, only use L/RINPUT3
+
+# ADC sync input
+SYNC_ADC = const(0)  # Use ADCLRC pin for ADC sync
+SYNC_DAC = const(1)  # used DACLRC pin for ADC sync
+
+# Protocol type
+BUS_I2S = const(2)  # I2S type
+BUS_LEFT_JUSTIFIED = const(1)  # Left justified mode
+BUS_RIGHT_JUSTIFIED = const(0)  # Right justified mode
+BUS_PCMA = const(3)  # PCM A mode
+BUS_PCMB = const(3 | (1 << 4))  # PCM B mode
+
+# Channel swap
+SWAP_NONE = const(0)
+SWAP_INPUT = const(1)
+SWAP_OUTPUT = const(2)
+
+# Mute settings
+MUTE_FAST = const(0)
+MUTE_SLOW = const(1)
+
+# ALC settings
+ALC_OFF = const(0)
+ALC_RIGHT = const(1)
+ALC_LEFT = const(2)
+ALC_STEREO = const(3)
+ALC_MODE = const(0)  # ALC mode
+ALC_LIMITER = const(1)  # Limiter mode
+
+# Clock Source
+SYSCLK_MCLK = const(0)  # sysclk source from external MCLK
+SYSCLK_PLL = const(1)  # sysclk source from internal PLL
+
+
+class Regs:
+    # register cache of 56 register. Since registers cannot be read back, they are
+    # kept in the table for modification
+    # fmt: off
+    cache = array.array("H", (
+        0x0097, 0x0097, 0x0000, 0x0000, 0x0000, 0x0008, 0x0000,
+        0x000a, 0x01c0, 0x0000, 0x00ff, 0x00ff, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x007b, 0x0100, 0x0032, 0x0000,
+        0x00c3, 0x00c3, 0x01c0, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0100, 0x0100, 0x0050,
+        0x0050, 0x0050, 0x0050, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0040, 0x0000, 0x0000, 0x0050, 0x0050, 0x0000, 0x0002,
+        0x0037, 0x004d, 0x0080, 0x0008, 0x0031, 0x0026, 0x00e9
+    ))
+    # fmt: on
+
+    def __init__(self, i2c, i2c_address=_I2C_ADDR):
+        self.value_buffer = bytearray(2)
+        self.i2c = i2c
+        self.i2c_address = i2c_address
+
+    def __getitem__(self, reg):
+        return self.cache[reg]
+
+    def __setitem__(self, reg, value):
+        if type(reg) is tuple:
+            if type(value) is tuple:
+                self[reg[0]] = value[0]
+                self[reg[1]] = value[1]
+            else:
+                self[reg[0]] = value
+                self[reg[1]] = value
+        else:
+            if type(value) is tuple:
+                val = (self.cache[reg] & (~value[0] & 0xFFFF)) | value[1]
+            else:
+                val = value
+            self.cache[reg] = val
+            self.value_buffer[0] = (reg << 1) | ((val >> 8) & 0x01)
+            self.value_buffer[1] = val & 0xFF
+            self.i2c.writeto(self.i2c_address, self.value_buffer)
+
+
+class WM8960:
+    _bit_clock_divider_table = {
+        2: 0,
+        3: 1,
+        4: 2,
+        6: 3,
+        8: 4,
+        11: 5,
+        12: 6,
+        16: 7,
+        22: 8,
+        24: 9,
+        32: 10,
+        44: 11,
+        48: 12,
+    }
+
+    _dac_divider_table = {
+        1.0 * 256: 0b000,
+        1.5 * 256: 0b001,
+        2 * 256: 0b010,
+        3 * 256: 0b011,
+        4 * 256: 0b100,
+        5.5 * 256: 0b101,
+        6 * 256: 0b110,
+    }
+
+    _audio_word_length_table = {
+        16: 0b00,
+        20: 0b01,
+        24: 0b10,
+        32: 0b11,
+    }
+
+    _alc_sample_rate_table = {
+        48000: 0,
+        44100: 0,
+        32000: 1,
+        24000: 2,
+        22050: 2,
+        16000: 3,
+        12000: 4,
+        11025: 4,
+        8000: 5,
+    }
+
+    _volume_config_table = {
+        MODULE_ADC: (_MAX_VOLUME_ADC, _LADC, 0x100),
+        MODULE_DAC: (_MAX_VOLUME_DAC, _LDAC, 0x100),
+        MODULE_HEADPHONE: (_MAX_VOLUME_HEADPHONE, _LOUT1, 0x180),
+        MODULE_LINE_IN: (_MAX_VOLUME_LINEIN, _LINVOL, 0x140),
+        MODULE_SPEAKER: (_MAX_VOLUME_SPEAKER, _LOUT2, 0x180),
+    }
+
+    _input_config_table = {
+        INPUT_CLOSED: None,
+        INPUT_MIC1: (0x138, 0x117),
+        INPUT_MIC2: (0x178, 0x117),
+        INPUT_MIC3: (0x1B8, 0x117),
+        INPUT_LINE2: (0, 0xE),
+        INPUT_LINE3: (0, 0x70),
+    }
+
+    def __init__(
+        self,
+        i2c,
+        sample_rate=16000,
+        bits=16,
+        swap=SWAP_NONE,
+        route=ROUTE_PLAYBACK_RECORD,
+        left_input=INPUT_MIC3,
+        right_input=INPUT_MIC2,
+        sysclk_source=SYSCLK_MCLK,
+        mclk_freq=None,
+        primary=False,
+        adc_sync=SYNC_DAC,
+        protocol=BUS_I2S,
+        i2c_address=_I2C_ADDR,
+    ):
+        self.regs = regs = Regs(i2c, i2c_address)
+        self.sample_rate = sample_rate
+
+        # check parameter consistency and set the sysclk value
+        if sysclk_source == SYSCLK_PLL:
+            if sample_rate in (11025, 22050, 44100):
+                sysclk = 11289600
+            else:
+                sysclk = 12288000
+            sysclk = max(sysclk, sample_rate * 256)
+            if mclk_freq is None:
+                mclk_freq = sysclk
+        else:  # sysclk_source == SYSCLK_MCLK
+            if mclk_freq is None:
+                mclk_freq = sample_rate * 256
+            sysclk = mclk_freq
+
+        regs[_RESET] = 0x00
+        # VMID=50K, Enable VREF, AINL, AINR, ADCL and ADCR
+        # I2S_IN (bit 0), I2S_OUT (bit 1), DAP (bit 4), DAC (bit 5), ADC (bit 6) are powered on
+        regs[_POWER1] = 0xFE
+        # Enable DACL, DACR, LOUT1, ROUT1, PLL down, SPKL, SPKR
+        regs[_POWER2] = 0x1F8
+        # Enable left and right channel input PGA, left and right output mixer
+        regs[_POWER3] = 0x3C
+
+        if adc_sync == SYNC_ADC:
+            # ADC and DAC use different Frame Clock Pins
+            regs[_IFACE2] = 0x00  # ADCLRC 0x00:Input 0x40:output.
+        else:
+            # ADC and DAC use the same Frame Clock Pin
+            regs[_IFACE2] = 0x40  # ADCLRC 0x00:Input 0x40:output.
+        self.set_data_route(route)
+        self.set_protocol(protocol)
+
+        if sysclk_source == SYSCLK_PLL:
+            self.set_internal_pll_config(mclk_freq, sysclk)
+        if primary:
+            self.set_master_clock(sysclk, sample_rate, bits)
+            # set master bit.
+            self.regs[_IFACE1] = (0, _IFACE1_MS_MASK)
+
+        self.set_speaker_clock(sysclk)
+
+        # swap channels
+        if swap & SWAP_INPUT:
+            regs[_IFACE1] = (0, _IFACE1_ALRSWAP_MASK)
+        if swap & SWAP_OUTPUT:
+            regs[_IFACE1] = (0, _IFACE1_DLRSWAP_MASK)
+
+        self.set_left_input(left_input)
+        self.set_right_input(right_input)
+
+        regs[_ADDCTL1] = 0x0C0
+        regs[_ADDCTL4] = 0x60  # Set GPIO1 to 0.
+
+        regs[_BYPASS1] = regs[_BYPASS2] = 0x0
+        # ADC volume, 0dB
+        regs[_LADC, _RADC] = 0x1C3
+        # Digital DAC volume, 0dB
+        regs[_LDAC, _RDAC] = 0x1FF
+        # Headphone volume, LOUT1 and ROUT1, 0dB
+        regs[_LOUT1, _ROUT1] = 0x16F
+        # speaker volume 6dB
+        regs[_LOUT2, _ROUT2] = 0x1FF
+        # enable class D output
+        regs[_CLASSD1] = 0xF7
+        # Unmute DAC.
+        regs[_DACCTL1] = 0x0000
+        # Input PGA volume 0 dB
+        regs[_LINVOL, _RINVOL] = 0x117
+
+        self.config_data_format(sysclk, sample_rate, bits)
+
+    def deinit(self):
+        self.set_module(MODULE_ADC, False)
+        self.set_module(MODULE_DAC, False)
+        self.set_module(MODULE_VREF, False)
+        self.set_module(MODULE_LINE_IN, False)
+        self.set_module(MODULE_LINE_OUT, False)
+        self.set_module(MODULE_SPEAKER, False)
+
+    def set_internal_pll_config(self, input_mclk, output_clk):
+        regs = self.regs
+        pllF2 = output_clk * 4
+        pll_prescale = 0
+        sysclk_div = 1
+        frac_mode = 0
+
+        # disable PLL power
+        regs[_POWER2] = (1, 0)
+        regs[_CLOCK1] = (7, 0)
+
+        pllN = pllF2 // input_mclk
+        if pllN < _PLL_N_MIN_VALUE:
+            input_mclk //= 2
+            pll_prescale = 1
+            pllN = pllF2 // input_mclk
+            if pllN < _PLL_N_MIN_VALUE:
+                sysclk_div = 2
+                pllF2 *= 2
+                pllN = pllF2 // input_mclk
+
+        if (pllN < _PLL_N_MIN_VALUE) or (pllN > _PLL_N_MAX_VALUE):
+            raise ValueError("Invalid MCLK vs. sysclk ratio")
+
+        pllK = ((pllF2 % input_mclk) * (1 << 24)) // input_mclk
+        if pllK != 0:
+            frac_mode = 1
+
+        regs[_PLL1] = (frac_mode << 5) | (pll_prescale << 4) | (pllN & 0x0F)
+        regs[_PLL2] = (pllK >> 16) & 0xFF
+        regs[_PLL3] = (pllK >> 8) & 0xFF
+        regs[_PLL4] = pllK & 0xFF
+        # enable PLL power
+        regs[_POWER2] = (1, 1)
+        regs[_CLOCK1] = (7, ((0 if sysclk_div == 1 else sysclk_div) << 1) | 1)
+
+    def set_master_clock(self, sysclk, sample_rate, bit_width):
+        bit_clock_divider = (sysclk * 2) // (sample_rate * bit_width * 2)
+        try:
+            reg_divider = self._bit_clock_divider_table[bit_clock_divider]
+        except:
+            raise ValueError("Invalid ratio of sysclk sample rate and bits")
+        # configure the master bit clock divider
+        self.regs[_CLOCK2] = (_CLOCK2_BCLK_DIV_MASK, reg_divider)
+
+    def set_speaker_clock(self, sysclk):
+        speaker_divider_table = (1.5, 2, 3, 4, 6, 8, 12, 16)
+        for val in range(8):
+            divider = speaker_divider_table[val]
+            f = sysclk / divider
+            if 500_000 < f < 1_000_000:
+                break
+        else:
+            val = 7
+        self.regs[_CLOCK2] = (
+            _CLOCK2_DCLK_DIV_MASK,
+            val << _CLOCK2_DCLK_DIV_SHIFT,
+        )
+
+    def set_module(self, module, is_enabled):
+        is_enabled = 1 if is_enabled else 0
+        regs = self.regs
+
+        if module == MODULE_ADC:
+            regs[_POWER1] = (
+                _POWER1_ADCL_MASK | _POWER1_ADCR_MASK,
+                (_POWER1_ADCL_MASK | _POWER1_ADCR_MASK) * is_enabled,
+            )
+
+        elif module == MODULE_DAC:
+            regs[_POWER2] = (
+                _POWER2_DACL_MASK | _POWER2_DACR_MASK,
+                (_POWER2_DACL_MASK | _POWER2_DACR_MASK) * is_enabled,
+            )
+
+        elif module == MODULE_VREF:
+            regs[_POWER1] = (
+                _POWER1_VREF_MASK,
+                (is_enabled << _POWER1_VREF_SHIFT),
+            )
+
+        elif module == MODULE_LINE_IN:
+            regs[_POWER1] = (
+                _POWER1_AINL_MASK | _POWER1_AINR_MASK,
+                (_POWER1_AINL_MASK | _POWER1_AINR_MASK) * is_enabled,
+            )
+            regs[_POWER3] = (
+                _POWER3_LMIC_MASK | _POWER3_RMIC_MASK,
+                (_POWER3_LMIC_MASK | _POWER3_RMIC_MASK) * is_enabled,
+            )
+
+        elif module == MODULE_LINE_OUT:
+            regs[_POWER2] = (
+                _POWER2_LOUT1_MASK | _POWER2_ROUT1_MASK,
+                (_POWER2_LOUT1_MASK | _POWER2_ROUT1_MASK) * is_enabled,
+            )
+
+        elif module == MODULE_MIC_BIAS:
+            regs[_POWER1] = (
+                _POWER1_MICB_MASK,
+                (is_enabled << _POWER1_MICB_SHIFT),
+            )
+
+        elif module == MODULE_SPEAKER:
+            regs[_POWER2] = (
+                _POWER2_SPKL_MASK | _POWER2_SPKR_MASK,
+                (_POWER2_SPKL_MASK | _POWER2_SPKR_MASK) * is_enabled,
+            )
+            regs[_CLASSD1] = 0xF7
+
+        elif module == MODULE_OMIX:
+            regs[_POWER3] = (
+                _POWER3_LOMIX_MASK | _POWER3_ROMIX_MASK,
+                (_POWER3_LOMIX_MASK | _POWER3_ROMIX_MASK) * is_enabled,
+            )
+
+        elif module == MODULE_MONO_OUT:
+            regs[_MONOMIX1] = regs[_MONOMIX2] = is_enabled << 7
+            regs[_MONO] = is_enabled << 6
+
+        else:
+            raise ValueError("Invalid module")
+
+    def enable_module(self, module):
+        self.set_module(module, True)
+
+    def disable_module(self, module):
+        self.set_module(module, False)
+
+    def set_data_route(self, route):
+        regs = self.regs
+        if route == ROUTE_BYPASS:
+            # Bypass means from line-in to HP
+            # Left LINPUT3 to left output mixer, LINPUT3 left output mixer volume = 0dB
+            # Right RINPUT3 to right output mixer, RINPUT3 right output mixer volume = 0dB
+            regs[_LOUTMIX, _ROUTMIX] = 0x80
+
+        elif route == ROUTE_PLAYBACK:
+            # Data route I2S_IN-> DAC-> HP
+            #
+            # Left DAC to left output mixer, LINPUT3 left output mixer volume = 0dB
+            # Right DAC to right output mixer, RINPUT3 right output mixer volume = 0dB
+            regs[_LOUTMIX, _ROUTMIX] = 0x100
+            regs[_POWER3] = 0x0C
+            # Set power for DAC
+            self.set_module(MODULE_DAC, True)
+            self.set_module(MODULE_OMIX, True)
+            self.set_module(MODULE_LINE_OUT, True)
+
+        elif route == ROUTE_PLAYBACK_RECORD:
+            #
+            # Left DAC to left output mixer, LINPUT3 left output mixer volume = 0dB
+            # Right DAC to right output mixer, RINPUT3 right output mixer volume = 0dB
+            regs[_LOUTMIX, _ROUTMIX] = 0x100
+            regs[_POWER3] = 0x3C
+            self.set_module(MODULE_DAC, True)
+            self.set_module(MODULE_ADC, True)
+            self.set_module(MODULE_LINE_IN, True)
+            self.set_module(MODULE_OMIX, True)
+            self.set_module(MODULE_LINE_OUT, True)
+
+        elif route == ROUTE_RECORD:
+            # LINE_IN->ADC->I2S_OUT
+            # Left and right input boost, LIN3BOOST and RIN3BOOST = 0dB
+            regs[_POWER3] = 0x30
+            # Power up ADC and AIN
+            self.set_module(MODULE_LINE_IN, True)
+            self.set_module(MODULE_ADC, True)
+
+        else:
+            raise ValueError("Invalid route")
+
+    def set_left_input(self, input):
+        if input not in self._input_config_table:
+            raise ValueError("Invalid input")
+
+        input = self._input_config_table[input]
+
+        regs = self.regs
+        if input is None:
+            regs[_POWER1] = (_POWER1_AINL_MASK | _POWER1_ADCL_MASK, 0)
+        elif input[0] == 0:
+            regs[_POWER1] = (0, _POWER1_AINL_MASK | _POWER1_ADCL_MASK)
+            regs[_INBMIX1] = input
+        else:
+            regs[_POWER1] = (0, _POWER1_AINL_MASK | _POWER1_ADCL_MASK | _POWER1_MICB_MASK)
+            regs[_LINPATH] = input[0]
+            regs[_LINVOL] = input[1]
+
+    def set_right_input(self, input):
+        if input not in self._input_config_table:
+            raise ValueError("Invalid input name")
+
+        input = self._input_config_table[input]
+
+        regs = self.regs
+        if input is None:
+            regs[_POWER1] = (_POWER1_AINR_MASK | _POWER1_ADCR_MASK, 0)
+        elif input[0] == 0:
+            regs[_POWER1] = (0, _POWER1_AINL_MASK | _POWER1_ADCR_MASK)
+            regs[_INBMIX2] = input
+        else:
+            regs[_POWER1] = (0, _POWER1_AINR_MASK | _POWER1_ADCR_MASK | _POWER1_MICB_MASK)
+            regs[_RINPATH] = input[0]
+            regs[_RINVOL] = input[1]
+
+    def set_protocol(self, protocol):
+        self.regs[_IFACE1] = (
+            _IFACE1_FORMAT_MASK | _IFACE1_LRP_MASK,
+            protocol,
+        )
+
+    def config_data_format(self, sysclk, sample_rate, bits):
+        #  Compute sample rate divider, dac and adc are the same sample rate
+        try:
+            divider = self._dac_divider_table[sysclk // sample_rate]
+            wl = self._audio_word_length_table[bits]
+        except:
+            raise ValueError("Invalid ratio sysclk/sample_rate or invalid bit length")
+
+        self.regs[_CLOCK1] = (0x1F8, divider << 6 | divider << 3)
+        self.regs[_IFACE1] = (_IFACE1_WL_MASK, wl << _IFACE1_WL_SHIFT)
+
+    def volume(self, module, volume_l=None, volume_r=None):
+        if module not in self._volume_config_table:
+            raise ValueError("Invalid module")
+
+        if volume_l is None:  # get volume
+            vol_max, regnum, _ = self._volume_config_table[module]
+            return (
+                int((self.regs[regnum] & vol_max) * 100 / vol_max + 0.5),
+                int((self.regs[regnum + 1] & vol_max) * 100 / vol_max + 0.5),
+            )
+        else:  # set volume
+            if volume_r is None:
+                volume_r = volume_l
+
+            if not ((0 <= volume_l <= 100) and (0 <= volume_r <= 100)):
+                raise ValueError("Invalid value for volume")
+            elif module not in self._volume_config_table:
+                raise ValueError("Invalid module")
+
+            vol_max, regnum, flags = self._volume_config_table[module]
+            self.regs[regnum] = int(volume_l * vol_max / 100 + 0.5) | flags
+            self.regs[regnum + 1] = int(volume_r * vol_max / 100 + 0.5) | flags
+
+    def mute(self, enable, soft=True, ramp=MUTE_FAST):
+        enable = _DACCTL1_DACMU_MASK if enable else 0
+        soft = _DACCTL2_DACSMM_MASK if soft else 0
+        ramp = _DACCTL2_DACMR_MASK if ramp == MUTE_SLOW else 0
+        self.regs[_DACCTL1] = (_DACCTL1_DACMU_MASK, enable)
+        self.regs[_DACCTL2] = (
+            _DACCTL2_DACSMM_MASK | _DACCTL2_DACMR_MASK,
+            soft | ramp,
+        )
+
+    def expand_3d(self, depth=0):
+        depth &= 0x0F
+        cutoff = 0 if self.sample_rate >= 32000 else 0b1100000
+        self.regs[_3D] = cutoff | depth << 1 | (1 if depth > 0 else 0)
+
+    def mono(self, enable):
+        enable = 1 if enable else 0
+        self.regs[_DACCTL1] = (
+            _DACCTL1_MONOMIX_MASK,
+            enable << _DACCTL1_MONOMIX_SHIFT,
+        )
+
+    def alc_mode(self, channel, mode=ALC_MODE):
+        if mode != ALC_MODE:
+            mode = ALC_LIMITER
+        channel &= 3
+        self.regs[_ALC1] = (
+            _ALC_CHANNEL_MASK,
+            channel << _ALC_CHANNEL_SHIFT,
+        )
+        self.regs[_ALC3] = (_ALC_MODE_MASK, mode << _ALC_MODE_SHIFT)
+        try:
+            rate = self._alc_sample_rate_table[self.sample_rate]
+        except:
+            rate = 0
+        self.regs[_ADDCTL3] = (_DACCTL3_ALCSR_MASK, rate)
+
+    def alc_gain(self, target=-12, max_gain=30, min_gain=-17.25, noise_gate=-78):
+        def limit(value, minval, maxval):
+            value = int(value)
+            value = max(value, minval)
+            value = min(value, maxval)
+            return value
+
+        target = limit((16 + (target * 2) // 3), 0, 15)
+        max_gain = limit((max_gain + 12) // 6, 0, 7)
+        min_gain = limit((min_gain * 4 + 69) // 24, 0, 7)
+        noise_gate = limit((noise_gate * 2 + 153) // 3, -1, 31)
+        self.regs[_ALC1] = (
+            _ALC_GAIN_MASK | _ALC_TARGET_MASK,
+            (max_gain << _ALC_GAIN_SHIFT) | target,
+        )
+        self.regs[_ALC2] = (_ALC_GAIN_MASK, (min_gain << _ALC_GAIN_SHIFT))
+        if noise_gate >= 0:
+            self.regs[_NOISEG] = noise_gate << _NOISEG_LEVEL_SHIFT | 1
+        else:
+            self.regs[_NOISEG] = 0
+
+    def alc_time(self, attack=24, decay=192, hold=0):
+        def logb(value, limit):
+            value = int(value)
+            lb = 0
+            while value > 1:
+                value >>= 1
+                lb += 1
+            lb = min(lb, limit)
+            return lb
+
+        attack = logb(attack / 6, 7)
+        decay = logb(decay / 24, 7)
+        hold = logb((hold * 3) / 8, 15)
+        self.regs[_ALC2] = (_ALC_HOLD_MASK, hold)
+        self.regs[_ALC3] = (
+            _ALC_DECAY_MASK | _ALC_ATTACK_MASK,
+            (decay << _ALC_DECAY_SHIFT) | attack,
+        )
+
+    def deemphasis(self, enable):
+        deem_table = (32000, 44100, 48000)
+        enable = not not enable
+        if enable and self.sample_rate in deem_table:
+            val = deem_table.index(self.sample_rate) + 1
+        else:
+            val = 0
+        self.regs[_DACCTL1] = (_DACCTL1_DEEM_MASK, val << _DACCTL1_DEEM_SHIFT)
diff --git a/micropython/drivers/display/lcd160cr/lcd160cr.py b/micropython/drivers/display/lcd160cr/lcd160cr.py
new file mode 100644
index 000000000..177c6fea3
--- /dev/null
+++ b/micropython/drivers/display/lcd160cr/lcd160cr.py
@@ -0,0 +1,481 @@
+# Driver for official MicroPython LCD160CR display
+# MIT license; Copyright (c) 2017 Damien P. George
+
+from micropython import const
+import machine
+from utime import sleep_ms
+from ustruct import calcsize, pack_into
+import errno
+
+# for set_orient
+PORTRAIT = const(0)
+LANDSCAPE = const(1)
+PORTRAIT_UPSIDEDOWN = const(2)
+LANDSCAPE_UPSIDEDOWN = const(3)
+
+# for set_startup_deco; can be or'd
+STARTUP_DECO_NONE = const(0)
+STARTUP_DECO_MLOGO = const(1)
+STARTUP_DECO_INFO = const(2)
+
+_uart_baud_table = {
+    2400: 0,
+    4800: 1,
+    9600: 2,
+    19200: 3,
+    38400: 4,
+    57600: 5,
+    115200: 6,
+    230400: 7,
+    460800: 8,
+}
+
+
+class LCD160CR:
+    def __init__(self, connect=None, *, pwr=None, i2c=None, spi=None, i2c_addr=98):
+        if connect in ("X", "Y", "XY", "YX"):
+            i = connect[-1]
+            j = connect[0]
+            y = j + "4"
+        elif connect == "C":
+            i = 2
+            j = 2
+            y = "A7"
+        else:
+            if pwr is None or i2c is None or spi is None:
+                raise ValueError('must specify valid "connect" or all of "pwr", "i2c" and "spi"')
+
+        if pwr is None:
+            pwr = machine.Pin(y, machine.Pin.OUT)
+        if i2c is None:
+            i2c = machine.I2C(i, freq=1000000)
+        if spi is None:
+            spi = machine.SPI(j, baudrate=13500000, polarity=0, phase=0)
+
+        if not pwr.value():
+            pwr(1)
+            sleep_ms(10)
+        # else:
+        # alread have power
+        # lets be optimistic...
+
+        # set connections
+        self.pwr = pwr
+        self.i2c = i2c
+        self.spi = spi
+        self.i2c_addr = i2c_addr
+
+        # create temp buffers and memoryviews
+        self.buf16 = bytearray(16)
+        self.buf19 = bytearray(19)
+        self.buf = [None] * 10
+        for i in range(1, 10):
+            self.buf[i] = memoryview(self.buf16)[0:i]
+        self.buf1 = self.buf[1]
+        self.array4 = [0, 0, 0, 0]
+
+        # set default orientation and window
+        self.set_orient(PORTRAIT)
+        self._fcmd2b("<BBBBBB", 0x76, 0, 0, self.w, self.h)  # viewport 'v'
+        self._fcmd2b("<BBBBBB", 0x79, 0, 0, self.w, self.h)  # window 'y'
+
+    def _send(self, cmd):
+        i = self.i2c.writeto(self.i2c_addr, cmd)
+        if i == len(cmd):
+            return
+        cmd = memoryview(cmd)
+        n = len(cmd)
+        while True:
+            i += self.i2c.writeto(self.i2c_addr, cmd[i:])
+            if i == n:
+                return
+            sleep_ms(10)
+
+    def _fcmd2(self, fmt, a0, a1=0, a2=0):
+        buf = self.buf[calcsize(fmt)]
+        pack_into(fmt, buf, 0, 2, a0, a1, a2)
+        self._send(buf)
+
+    def _fcmd2b(self, fmt, a0, a1, a2, a3, a4=0):
+        buf = self.buf[calcsize(fmt)]
+        pack_into(fmt, buf, 0, 2, a0, a1, a2, a3, a4)
+        self._send(buf)
+
+    def _waitfor(self, n, buf):
+        t = 5000
+        while t:
+            self.i2c.readfrom_into(self.i2c_addr, self.buf1)
+            if self.buf1[0] >= n:
+                self.i2c.readfrom_into(self.i2c_addr, buf)
+                return
+            t -= 1
+            sleep_ms(1)
+        raise OSError(errno.ETIMEDOUT)
+
+    def oflush(self, n=255):
+        t = 5000
+        while t:
+            self.i2c.readfrom_into(self.i2c_addr + 1, self.buf1)
+            r = self.buf1[0]
+            if r >= n:
+                return
+            t -= 1
+            machine.idle()
+        raise OSError(errno.ETIMEDOUT)
+
+    def iflush(self):
+        t = 5000
+        while t:
+            self.i2c.readfrom_into(self.i2c_addr, self.buf16)
+            if self.buf16[0] == 0:
+                return
+            t -= 1
+            sleep_ms(1)
+        raise OSError(errno.ETIMEDOUT)
+
+    #### MISC METHODS ####
+
+    @staticmethod
+    def rgb(r, g, b):
+        return ((b & 0xF8) << 8) | ((g & 0xFC) << 3) | (r >> 3)
+
+    @staticmethod
+    def clip_line(c, w, h):
+        while True:
+            ca = ce = 0
+            if c[1] < 0:
+                ca |= 8
+            elif c[1] > h:
+                ca |= 4
+            if c[0] < 0:
+                ca |= 1
+            elif c[0] > w:
+                ca |= 2
+            if c[3] < 0:
+                ce |= 8
+            elif c[3] > h:
+                ce |= 4
+            if c[2] < 0:
+                ce |= 1
+            elif c[2] > w:
+                ce |= 2
+            if ca & ce:
+                return False
+            elif ca | ce:
+                ca |= ce
+                if ca & 1:
+                    if c[2] < c[0]:
+                        c[0], c[2] = c[2], c[0]
+                        c[1], c[3] = c[3], c[1]
+                    c[1] += ((-c[0]) * (c[3] - c[1])) // (c[2] - c[0])
+                    c[0] = 0
+                elif ca & 2:
+                    if c[2] < c[0]:
+                        c[0], c[2] = c[2], c[0]
+                        c[1], c[3] = c[3], c[1]
+                    c[3] += ((w - 1 - c[2]) * (c[3] - c[1])) // (c[2] - c[0])
+                    c[2] = w - 1
+                elif ca & 4:
+                    if c[0] == c[2]:
+                        if c[1] >= h:
+                            c[1] = h - 1
+                        if c[3] >= h:
+                            c[3] = h - 1
+                    else:
+                        if c[3] < c[1]:
+                            c[0], c[2] = c[2], c[0]
+                            c[1], c[3] = c[3], c[1]
+                        c[2] += ((h - 1 - c[3]) * (c[2] - c[0])) // (c[3] - c[1])
+                        c[3] = h - 1
+                else:
+                    if c[0] == c[2]:
+                        c[1] = max(c[1], 0)
+                        c[3] = max(c[3], 0)
+                    else:
+                        if c[3] < c[1]:
+                            c[0], c[2] = c[2], c[0]
+                            c[1], c[3] = c[3], c[1]
+                        c[0] += ((-c[1]) * (c[2] - c[0])) // (c[3] - c[1])
+                        c[1] = 0
+            else:
+                return True
+
+    #### SETUP COMMANDS ####
+
+    def set_power(self, on):
+        self.pwr(on)
+        sleep_ms(15)
+
+    def set_orient(self, orient):
+        self._fcmd2("<BBB", 0x14, (orient & 3) + 4)
+        # update width and height variables
+        self.iflush()
+        self._send(b"\x02g0")
+        self._waitfor(4, self.buf[5])
+        self.w = self.buf[5][1]
+        self.h = self.buf[5][2]
+
+    def set_brightness(self, value):
+        self._fcmd2("<BBB", 0x16, value)
+
+    def set_i2c_addr(self, addr):
+        # 0x0e set i2c addr
+        if addr & 3:
+            raise ValueError("must specify mod 4 aligned address")
+        self._fcmd2("<BBW", 0x0E, 0x433249 | (addr << 24))
+
+    def set_uart_baudrate(self, baudrate):
+        try:
+            baudrate = _uart_baud_table[baudrate]
+        except KeyError:
+            raise ValueError("invalid baudrate")
+        self._fcmd2("<BBB", 0x18, baudrate)
+
+    def set_startup_deco(self, value):
+        self._fcmd2("<BBB", 0x19, value)
+
+    def save_to_flash(self):
+        self._send(b"\x02fn")
+
+    #### PIXEL ACCESS ####
+
+    def set_pixel(self, x, y, c):
+        self._fcmd2b("<BBBBH", 0x41, x, y, c)
+
+    def get_pixel(self, x, y):
+        self._fcmd2("<BBBB", 0x61, x, y)
+        t = 1000
+        while t:
+            self.i2c.readfrom_into(self.i2c_addr, self.buf1)
+            if self.buf1[0] >= 2:
+                self.i2c.readfrom_into(self.i2c_addr, self.buf[3])
+                return self.buf[3][1] | self.buf[3][2] << 8
+            t -= 1
+            sleep_ms(1)
+        raise OSError(errno.ETIMEDOUT)
+
+    def get_line(self, x, y, buf):
+        l = len(buf) // 2
+        self._fcmd2b("<BBBBB", 0x10, l, x, y)
+        l *= 2
+        t = 1000
+        while t:
+            self.i2c.readfrom_into(self.i2c_addr, self.buf1)
+            if self.buf1[0] >= l:
+                self.i2c.readfrom_into(self.i2c_addr, buf)
+                return
+            t -= 1
+            sleep_ms(1)
+        raise OSError(errno.ETIMEDOUT)
+
+    def screen_dump(self, buf, x=0, y=0, w=None, h=None):
+        if w is None:
+            w = self.w - x
+        if h is None:
+            h = self.h - y
+        if w <= 127:
+            line = bytearray(2 * w + 1)
+            line2 = None
+        else:
+            # split line if more than 254 bytes needed
+            buflen = (w + 1) // 2
+            line = bytearray(2 * buflen + 1)
+            line2 = memoryview(line)[: 2 * (w - buflen) + 1]
+        for i in range(min(len(buf) // (2 * w), h)):
+            ix = i * w * 2
+            self.get_line(x, y + i, line)
+            buf[ix : ix + len(line) - 1] = memoryview(line)[1:]
+            ix += len(line) - 1
+            if line2:
+                self.get_line(x + buflen, y + i, line2)
+                buf[ix : ix + len(line2) - 1] = memoryview(line2)[1:]
+                ix += len(line2) - 1
+
+    def screen_load(self, buf):
+        l = self.w * self.h * 2 + 2
+        self._fcmd2b("<BBHBBB", 0x70, l, 16, self.w, self.h)
+        n = 0
+        ar = memoryview(buf)
+        while n < len(buf):
+            if len(buf) - n >= 0x200:
+                self._send(ar[n : n + 0x200])
+                n += 0x200
+            else:
+                self._send(ar[n:])
+                while n < self.w * self.h * 2:
+                    self._send(b"\x00")
+                    n += 1
+
+    #### TEXT COMMANDS ####
+
+    def set_pos(self, x, y):
+        self._fcmd2("<BBBB", 0x58, x, y)
+
+    def set_text_color(self, fg, bg):
+        self._fcmd2("<BBHH", 0x63, fg, bg)
+
+    def set_font(self, font, scale=0, bold=0, trans=0, scroll=0):
+        self._fcmd2(
+            "<BBBB",
+            0x46,
+            (scroll << 7) | (trans << 6) | ((font & 3) << 4) | (bold & 0xF),
+            scale & 0xFF,
+        )
+
+    def write(self, s):
+        # TODO: eventually check for room in LCD input queue
+        self._send(s)
+
+    #### PRIMITIVE DRAWING COMMANDS ####
+
+    def set_pen(self, line, fill):
+        self._fcmd2("<BBHH", 0x50, line, fill)
+
+    def erase(self):
+        self._send(b"\x02\x45")
+
+    def dot(self, x, y):
+        if 0 <= x < self.w and 0 <= y < self.h:
+            self._fcmd2("<BBBB", 0x4B, x, y)
+
+    def rect(self, x, y, w, h, cmd=0x72):
+        if x + w <= 0 or y + h <= 0 or x >= self.w or y >= self.h:
+            return
+        elif x < 0 or y < 0:
+            left = top = True
+            if x < 0:
+                left = False
+                w += x
+                x = 0
+            if y < 0:
+                top = False
+                h += y
+                y = 0
+            if cmd == 0x51 or cmd == 0x72:
+                # draw interior
+                self._fcmd2b("<BBBBBB", 0x51, x, y, min(w, 255), min(h, 255))
+            if cmd == 0x57 or cmd == 0x72:
+                # draw outline
+                if left:
+                    self._fcmd2b("<BBBBBB", 0x57, x, y, 1, min(h, 255))
+                if top:
+                    self._fcmd2b("<BBBBBB", 0x57, x, y, min(w, 255), 1)
+                if x + w < self.w:
+                    self._fcmd2b("<BBBBBB", 0x57, x + w, y, 1, min(h, 255))
+                if y + h < self.h:
+                    self._fcmd2b("<BBBBBB", 0x57, x, y + h, min(w, 255), 1)
+        else:
+            self._fcmd2b("<BBBBBB", cmd, x, y, min(w, 255), min(h, 255))
+
+    def rect_outline(self, x, y, w, h):
+        self.rect(x, y, w, h, 0x57)
+
+    def rect_interior(self, x, y, w, h):
+        self.rect(x, y, w, h, 0x51)
+
+    def line(self, x1, y1, x2, y2):
+        ar4 = self.array4
+        ar4[0] = x1
+        ar4[1] = y1
+        ar4[2] = x2
+        ar4[3] = y2
+        if self.clip_line(ar4, self.w, self.h):
+            self._fcmd2b("<BBBBBB", 0x4C, ar4[0], ar4[1], ar4[2], ar4[3])
+
+    def dot_no_clip(self, x, y):
+        self._fcmd2("<BBBB", 0x4B, x, y)
+
+    def rect_no_clip(self, x, y, w, h):
+        self._fcmd2b("<BBBBBB", 0x72, x, y, w, h)
+
+    def rect_outline_no_clip(self, x, y, w, h):
+        self._fcmd2b("<BBBBBB", 0x57, x, y, w, h)
+
+    def rect_interior_no_clip(self, x, y, w, h):
+        self._fcmd2b("<BBBBBB", 0x51, x, y, w, h)
+
+    def line_no_clip(self, x1, y1, x2, y2):
+        self._fcmd2b("<BBBBBB", 0x4C, x1, y1, x2, y2)
+
+    def poly_dot(self, data):
+        if len(data) & 1:
+            raise ValueError("must specify even number of bytes")
+        self._fcmd2("<BBB", 0x71, len(data) // 2)
+        self._send(data)
+
+    def poly_line(self, data):
+        if len(data) & 1:
+            raise ValueError("must specify even number of bytes")
+        self._fcmd2("<BBB", 0x78, len(data) // 2)
+        self._send(data)
+
+    #### TOUCH COMMANDS ####
+
+    def touch_config(self, calib=False, save=False, irq=None):
+        self._fcmd2("<BBBB", 0x7A, (irq is not None) << 2 | save << 1 | calib, bool(irq) << 7)
+
+    def is_touched(self):
+        self._send(b"\x02T")
+        b = self.buf[4]
+        self._waitfor(3, b)
+        return b[1] >> 7 != 0
+
+    def get_touch(self):
+        self._send(b"\x02T")  # implicit LCD output flush
+        b = self.buf[4]
+        self._waitfor(3, b)
+        return b[1] >> 7, b[2], b[3]
+
+    #### ADVANCED COMMANDS ####
+
+    def set_spi_win(self, x, y, w, h):
+        pack_into(
+            "<BBBHHHHHHHH", self.buf19, 0, 2, 0x55, 10, x, y, x + w - 1, y + h - 1, 0, 0, 0, 0xFFFF
+        )
+        self._send(self.buf19)
+
+    def fast_spi(self, flush=True):
+        self._send(b"\x02\x12")
+        if flush:
+            self.oflush()
+        return self.spi
+
+    def show_framebuf(self, buf):
+        self.fast_spi().write(buf)
+
+    def set_scroll(self, on):
+        self._fcmd2("<BBB", 0x15, on)
+
+    def set_scroll_win(self, win, x=-1, y=0, w=0, h=0, vec=0, pat=0, fill=0x07E0, color=0):
+        pack_into("<BBBHHHHHHHH", self.buf19, 0, 2, 0x55, win, x, y, w, h, vec, pat, fill, color)
+        self._send(self.buf19)
+
+    def set_scroll_win_param(self, win, param, value):
+        self._fcmd2b("<BBBBH", 0x75, win, param, value)
+
+    def set_scroll_buf(self, s):
+        l = len(s)
+        if l > 32:
+            raise ValueError("length must be 32 or less")
+        self._fcmd2("<BBB", 0x11, l)
+        self._send(s)
+
+    def jpeg_start(self, l):
+        if l > 0xFFFF:
+            raise ValueError("length must be 65535 or less")
+        self.oflush()
+        self._fcmd2("<BBH", 0x6A, l)
+
+    def jpeg_data(self, buf):
+        self._send(buf)
+
+    def jpeg(self, buf):
+        self.jpeg_start(len(buf))
+        self.jpeg_data(buf)
+
+    def feed_wdt(self):
+        self._send(b"\x02\x17")
+
+    def reset(self):
+        self._send(b"\x02Y\xef\xbe\xad\xde")
+        sleep_ms(15)
diff --git a/micropython/drivers/display/lcd160cr/lcd160cr_test.py b/micropython/drivers/display/lcd160cr/lcd160cr_test.py
new file mode 100644
index 000000000..b2a9c0c6a
--- /dev/null
+++ b/micropython/drivers/display/lcd160cr/lcd160cr_test.py
@@ -0,0 +1,190 @@
+# Driver test for official MicroPython LCD160CR display
+# MIT license; Copyright (c) 2017 Damien P. George
+
+import framebuf
+import lcd160cr
+import math
+import time
+
+
+def get_lcd(lcd):
+    if isinstance(lcd, str):
+        lcd = lcd160cr.LCD160CR(lcd)
+    return lcd
+
+
+def show_adc(lcd, adc):
+    data = [adc.read_core_temp(), adc.read_core_vbat(), 3.3]
+    try:
+        data[2] = adc.read_vref()
+    except:
+        pass
+    for i in range(3):
+        lcd.set_text_color((825, 1625, 1600)[i], 0)
+        if lcd.h == 160:
+            lcd.set_font(2)
+            lcd.set_pos(0, 100 + i * 16)
+        else:
+            lcd.set_font(2, trans=1)
+            lcd.set_pos(0, lcd.h - 60 + i * 16)
+        lcd.write("%4s: " % ("TEMP", "VBAT", "VREF")[i])
+        if i > 0:
+            s = "%6.3fV" % data[i]
+        else:
+            s = "%5.1f°C" % data[i]
+        if lcd.h == 160:
+            lcd.set_font(1, bold=0, scale=1)
+        else:
+            lcd.set_font(1, bold=0, scale=1, trans=1)
+            lcd.set_pos(45, lcd.h - 60 + i * 16)
+        lcd.write(s)
+
+
+def test_features(lcd, orient=lcd160cr.PORTRAIT):
+    # if we run on pyboard then use ADC and RTC features
+    try:
+        import pyb
+
+        adc = pyb.ADCAll(12, 0xF0000)
+        rtc = pyb.RTC()
+    except:
+        adc = None
+        rtc = None
+
+    # set orientation and clear screen
+    lcd = get_lcd(lcd)
+    lcd.set_orient(orient)
+    lcd.set_pen(0, 0)
+    lcd.erase()
+
+    # create M-logo
+    mlogo = framebuf.FrameBuffer(bytearray(17 * 17 * 2), 17, 17, framebuf.RGB565)
+    mlogo.fill(0)
+    mlogo.fill_rect(1, 1, 15, 15, 0xFFFFFF)
+    mlogo.vline(4, 4, 12, 0)
+    mlogo.vline(8, 1, 12, 0)
+    mlogo.vline(12, 4, 12, 0)
+    mlogo.vline(14, 13, 2, 0)
+
+    # create inline framebuf
+    offx = 14
+    offy = 19
+    w = 100
+    h = 75
+    fbuf = framebuf.FrameBuffer(bytearray(w * h * 2), w, h, framebuf.RGB565)
+    lcd.set_spi_win(offx, offy, w, h)
+
+    # initialise loop parameters
+    tx = ty = 0
+    t0 = time.ticks_us()
+
+    for i in range(300):
+        # update position of cross-hair
+        t, tx2, ty2 = lcd.get_touch()
+        if t:
+            tx2 -= offx
+            ty2 -= offy
+            if tx2 >= 0 and ty2 >= 0 and tx2 < w and ty2 < h:
+                tx, ty = tx2, ty2
+        else:
+            tx = (tx + 1) % w
+            ty = (ty + 1) % h
+
+        # create and show the inline framebuf
+        fbuf.fill(lcd.rgb(128 + int(64 * math.cos(0.1 * i)), 128, 192))
+        fbuf.line(
+            w // 2,
+            h // 2,
+            w // 2 + int(40 * math.cos(0.2 * i)),
+            h // 2 + int(40 * math.sin(0.2 * i)),
+            lcd.rgb(128, 255, 64),
+        )
+        fbuf.hline(0, ty, w, lcd.rgb(64, 64, 64))
+        fbuf.vline(tx, 0, h, lcd.rgb(64, 64, 64))
+        fbuf.rect(tx - 3, ty - 3, 7, 7, lcd.rgb(64, 64, 64))
+        for phase in (-0.2, 0, 0.2):
+            x = w // 2 - 8 + int(50 * math.cos(0.05 * i + phase))
+            y = h // 2 - 8 + int(32 * math.sin(0.05 * i + phase))
+            fbuf.blit(mlogo, x, y)
+        for j in range(-3, 3):
+            fbuf.text(
+                "MicroPython",
+                5,
+                h // 2 + 9 * j + int(20 * math.sin(0.1 * (i + j))),
+                lcd.rgb(128 + 10 * j, 0, 128 - 10 * j),
+            )
+        lcd.show_framebuf(fbuf)
+
+        # show results from the ADC
+        if adc:
+            show_adc(lcd, adc)
+
+        # show the time
+        if rtc:
+            lcd.set_pos(2, 0)
+            lcd.set_font(1)
+            t = rtc.datetime()
+            lcd.write(
+                "%4d-%02d-%02d %2d:%02d:%02d.%01d"
+                % (t[0], t[1], t[2], t[4], t[5], t[6], t[7] // 100000)
+            )
+
+        # compute the frame rate
+        t1 = time.ticks_us()
+        dt = time.ticks_diff(t1, t0)
+        t0 = t1
+
+        # show the frame rate
+        lcd.set_pos(2, 9)
+        lcd.write("%.2f fps" % (1000000 / dt))
+
+
+def test_mandel(lcd, orient=lcd160cr.PORTRAIT):
+    # set orientation and clear screen
+    lcd = get_lcd(lcd)
+    lcd.set_orient(orient)
+    lcd.set_pen(0, 0xFFFF)
+    lcd.erase()
+
+    # function to compute Mandelbrot pixels
+    def in_set(c):
+        z = 0
+        for i in range(32):
+            z = z * z + c
+            if abs(z) > 100:
+                return i
+        return 0
+
+    # cache width and height of LCD
+    w = lcd.w
+    h = lcd.h
+
+    # create the buffer for each line and set SPI parameters
+    line = bytearray(w * 2)
+    lcd.set_spi_win(0, 0, w, h)
+    spi = lcd.fast_spi()
+
+    # draw the Mandelbrot set line-by-line
+    hh = (h - 1) / 3.2
+    ww = (w - 1) / 2.4
+    for v in range(h):
+        for u in range(w):
+            c = in_set((v / hh - 2.3) + (u / ww - 1.2) * 1j)
+            if c < 16:
+                rgb = c << 12 | c << 6
+            else:
+                rgb = 0xF800 | c << 6
+            line[2 * u] = rgb
+            line[2 * u + 1] = rgb >> 8
+        spi.write(line)
+
+
+def test_all(lcd, orient=lcd160cr.PORTRAIT):
+    lcd = get_lcd(lcd)
+    test_features(lcd, orient)
+    test_mandel(lcd, orient)
+
+
+print("To run all tests: test_all(<lcd>)")
+print("Individual tests are: test_features, test_mandel")
+print('<lcd> argument should be a connection, eg "X", or an LCD160CR object')
diff --git a/micropython/drivers/display/lcd160cr/manifest.py b/micropython/drivers/display/lcd160cr/manifest.py
new file mode 100644
index 000000000..9e18a02a7
--- /dev/null
+++ b/micropython/drivers/display/lcd160cr/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="LCD160CR driver.", version="0.1.0")
+
+module("lcd160cr.py", opt=3)
diff --git a/micropython/drivers/display/ssd1306/manifest.py b/micropython/drivers/display/ssd1306/manifest.py
new file mode 100644
index 000000000..80253be44
--- /dev/null
+++ b/micropython/drivers/display/ssd1306/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="SSD1306 OLED driver.", version="0.1.0")
+
+module("ssd1306.py", opt=3)
diff --git a/micropython/drivers/display/ssd1306/ssd1306.py b/micropython/drivers/display/ssd1306/ssd1306.py
new file mode 100644
index 000000000..37ad682de
--- /dev/null
+++ b/micropython/drivers/display/ssd1306/ssd1306.py
@@ -0,0 +1,164 @@
+# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
+
+from micropython import const
+import framebuf
+
+
+# register definitions
+SET_CONTRAST = const(0x81)
+SET_ENTIRE_ON = const(0xA4)
+SET_NORM_INV = const(0xA6)
+SET_DISP = const(0xAE)
+SET_MEM_ADDR = const(0x20)
+SET_COL_ADDR = const(0x21)
+SET_PAGE_ADDR = const(0x22)
+SET_DISP_START_LINE = const(0x40)
+SET_SEG_REMAP = const(0xA0)
+SET_MUX_RATIO = const(0xA8)
+SET_IREF_SELECT = const(0xAD)
+SET_COM_OUT_DIR = const(0xC0)
+SET_DISP_OFFSET = const(0xD3)
+SET_COM_PIN_CFG = const(0xDA)
+SET_DISP_CLK_DIV = const(0xD5)
+SET_PRECHARGE = const(0xD9)
+SET_VCOM_DESEL = const(0xDB)
+SET_CHARGE_PUMP = const(0x8D)
+
+
+# Subclassing FrameBuffer provides support for graphics primitives
+# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
+class SSD1306(framebuf.FrameBuffer):
+    def __init__(self, width, height, external_vcc):
+        self.width = width
+        self.height = height
+        self.external_vcc = external_vcc
+        self.pages = self.height // 8
+        self.buffer = bytearray(self.pages * self.width)
+        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
+        self.init_display()
+
+    def init_display(self):
+        for cmd in (
+            SET_DISP,  # display off
+            # address setting
+            SET_MEM_ADDR,
+            0x00,  # horizontal
+            # resolution and layout
+            SET_DISP_START_LINE,  # start at line 0
+            SET_SEG_REMAP | 0x01,  # column addr 127 mapped to SEG0
+            SET_MUX_RATIO,
+            self.height - 1,
+            SET_COM_OUT_DIR | 0x08,  # scan from COM[N] to COM0
+            SET_DISP_OFFSET,
+            0x00,
+            SET_COM_PIN_CFG,
+            0x02 if self.width > 2 * self.height else 0x12,
+            # timing and driving scheme
+            SET_DISP_CLK_DIV,
+            0x80,
+            SET_PRECHARGE,
+            0x22 if self.external_vcc else 0xF1,
+            SET_VCOM_DESEL,
+            0x30,  # 0.83*Vcc
+            # display
+            SET_CONTRAST,
+            0xFF,  # maximum
+            SET_ENTIRE_ON,  # output follows RAM contents
+            SET_NORM_INV,  # not inverted
+            SET_IREF_SELECT,
+            0x30,  # enable internal IREF during display on
+            # charge pump
+            SET_CHARGE_PUMP,
+            0x10 if self.external_vcc else 0x14,
+            SET_DISP | 0x01,  # display on
+        ):  # on
+            self.write_cmd(cmd)
+        self.fill(0)
+        self.show()
+
+    def poweroff(self):
+        self.write_cmd(SET_DISP)
+
+    def poweron(self):
+        self.write_cmd(SET_DISP | 0x01)
+
+    def contrast(self, contrast):
+        self.write_cmd(SET_CONTRAST)
+        self.write_cmd(contrast)
+
+    def invert(self, invert):
+        self.write_cmd(SET_NORM_INV | (invert & 1))
+
+    def rotate(self, rotate):
+        self.write_cmd(SET_COM_OUT_DIR | ((rotate & 1) << 3))
+        self.write_cmd(SET_SEG_REMAP | (rotate & 1))
+
+    def show(self):
+        x0 = 0
+        x1 = self.width - 1
+        if self.width != 128:
+            # narrow displays use centred columns
+            col_offset = (128 - self.width) // 2
+            x0 += col_offset
+            x1 += col_offset
+        self.write_cmd(SET_COL_ADDR)
+        self.write_cmd(x0)
+        self.write_cmd(x1)
+        self.write_cmd(SET_PAGE_ADDR)
+        self.write_cmd(0)
+        self.write_cmd(self.pages - 1)
+        self.write_data(self.buffer)
+
+
+class SSD1306_I2C(SSD1306):
+    def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
+        self.i2c = i2c
+        self.addr = addr
+        self.temp = bytearray(2)
+        self.write_list = [b"\x40", None]  # Co=0, D/C#=1
+        super().__init__(width, height, external_vcc)
+
+    def write_cmd(self, cmd):
+        self.temp[0] = 0x80  # Co=1, D/C#=0
+        self.temp[1] = cmd
+        self.i2c.writeto(self.addr, self.temp)
+
+    def write_data(self, buf):
+        self.write_list[1] = buf
+        self.i2c.writevto(self.addr, self.write_list)
+
+
+class SSD1306_SPI(SSD1306):
+    def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
+        self.rate = 10 * 1024 * 1024
+        dc.init(dc.OUT, value=0)
+        res.init(res.OUT, value=0)
+        cs.init(cs.OUT, value=1)
+        self.spi = spi
+        self.dc = dc
+        self.res = res
+        self.cs = cs
+        import time
+
+        self.res(1)
+        time.sleep_ms(1)
+        self.res(0)
+        time.sleep_ms(10)
+        self.res(1)
+        super().__init__(width, height, external_vcc)
+
+    def write_cmd(self, cmd):
+        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
+        self.cs(1)
+        self.dc(0)
+        self.cs(0)
+        self.spi.write(bytearray([cmd]))
+        self.cs(1)
+
+    def write_data(self, buf):
+        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
+        self.cs(1)
+        self.dc(1)
+        self.cs(0)
+        self.spi.write(buf)
+        self.cs(1)
diff --git a/micropython/drivers/imu/bmi270/bmi270.py b/micropython/drivers/imu/bmi270/bmi270.py
new file mode 100644
index 000000000..64f819ec2
--- /dev/null
+++ b/micropython/drivers/imu/bmi270/bmi270.py
@@ -0,0 +1,634 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Basic example usage:
+
+import time
+from bmi270 import BMI270
+from machine import Pin, SPI, I2C
+
+# Init in I2C mode.
+imu = BMI270(I2C(1, scl=Pin(15), sda=Pin(14)))
+
+# Or init in SPI mode.
+# TODO: Not supported yet.
+# imu = BMI270(SPI(5), cs=Pin(10))
+
+while (True):
+    print('Accelerometer: x:{:>6.3f}  y:{:>6.3f}  z:{:>6.3f}'.format(*imu.accel()))
+    print('Gyroscope:     x:{:>6.3f}  y:{:>6.3f}  z:{:>6.3f}'.format(*imu.gyro()))
+    print('Magnetometer:  x:{:>8.3f}  y:{:>8.3f}  z:{:>8.3f}'.format(*imu.magnet()))
+    print("")
+    time.sleep_ms(100)
+"""
+
+import array
+import time
+from micropython import const
+
+_DEFAULT_ADDR = const(0x68)
+_CHIP_ID = const(0x00)
+_STATUS = const(0x21)
+_INIT_ADDR_0 = const(0x5B)
+_INIT_ADDR_1 = const(0x5C)
+_DATA_8 = const(0x0C)
+_DATA_14 = const(0x12)
+_CMD = const(0x7E)
+_CONFIG_DATA = const(
+    b"\xc8\x2e\x00\x2e\x80\x2e\x3d\xb1\xc8\x2e\x00\x2e\x80\x2e\x91\x03\x80\x2e\xbc"
+    b"\xb0\x80\x2e\xa3\x03\xc8\x2e\x00\x2e\x80\x2e\x00\xb0\x50\x30\x21\x2e\x59\xf5"
+    b"\x10\x30\x21\x2e\x6a\xf5\x80\x2e\x3b\x03\x00\x00\x00\x00\x08\x19\x01\x00\x22"
+    b"\x00\x75\x00\x00\x10\x00\x10\xd1\x00\xb3\x43\x80\x2e\x00\xc1\x80\x2e\x00\xc1"
+    b"\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00"
+    b"\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e"
+    b"\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80"
+    b"\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1"
+    b"\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00"
+    b"\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e"
+    b"\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80"
+    b"\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1"
+    b"\x80\x2e\x00\xc1\x80\x2e\x00\xc1\xe0\x5f\x00\x00\x00\x00\x01\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x92\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x08\x19\x00\x00\x88\x00\x00\x00\x00\x00\x00\x00\x05"
+    b"\xe0\xaa\x38\x05\xe0\x90\x30\xfa\x00\x96\x00\x4b\x09\x11\x00\x11\x00\x02\x00"
+    b"\x2d\x01\xd4\x7b\x3b\x01\xdb\x7a\x04\x00\x3f\x7b\xcd\x6c\xc3\x04\x85\x09\xc3"
+    b"\x04\xec\xe6\x0c\x46\x01\x00\x27\x00\x19\x00\x96\x00\xa0\x00\x01\x00\x0c\x00"
+    b"\xf0\x3c\x00\x01\x01\x00\x03\x00\x01\x00\x0e\x00\x00\x00\x32\x00\x05\x00\xee"
+    b"\x06\x04\x00\xc8\x00\x00\x00\x04\x00\xa8\x05\xee\x06\x00\x04\xbc\x02\xb3\x00"
+    b"\x85\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\xb4\x00\x01\x00\xb9\x00\x01\x00\x98\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x01\x00\x80\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x2e\x00\xc1\xfd\x2d\xde"
+    b"\x00\xeb\x00\xda\x00\x00\x0c\xff\x0f\x00\x04\xc0\x00\x5b\xf5\xc9\x01\x1e\xf2"
+    b"\x80\x00\x3f\xff\x19\xf4\x58\xf5\x66\xf5\x64\xf5\xc0\xf1\xf0\x00\xe0\x00\xcd"
+    b"\x01\xd3\x01\xdb\x01\xff\x7f\xff\x01\xe4\x00\x74\xf7\xf3\x00\xfa\x00\xff\x3f"
+    b"\xca\x03\x6c\x38\x56\xfe\x44\xfd\xbc\x02\xf9\x06\x00\xfc\x12\x02\xae\x01\x58"
+    b"\xfa\x9a\xfd\x77\x05\xbb\x02\x96\x01\x95\x01\x7f\x01\x82\x01\x89\x01\x87\x01"
+    b"\x88\x01\x8a\x01\x8c\x01\x8f\x01\x8d\x01\x92\x01\x91\x01\xdd\x00\x9f\x01\x7e"
+    b"\x01\xdb\x00\xb6\x01\x70\x69\x26\xd3\x9c\x07\x1f\x05\x9d\x00\x00\x08\xbc\x05"
+    b"\x37\xfa\xa2\x01\xaa\x01\xa1\x01\xa8\x01\xa0\x01\xa8\x05\xb4\x01\xb4\x01\xce"
+    b"\x00\xd0\x00\xfc\x00\xc5\x01\xff\xfb\xb1\x00\x00\x38\x00\x30\xfd\xf5\xfc\xf5"
+    b"\xcd\x01\xa0\x00\x5f\xff\x00\x40\xff\x00\x00\x80\x6d\x0f\xeb\x00\x7f\xff\xc2"
+    b"\xf5\x68\xf7\xb3\xf1\x67\x0f\x5b\x0f\x61\x0f\x80\x0f\x58\xf7\x5b\xf7\x83\x0f"
+    b"\x86\x00\x72\x0f\x85\x0f\xc6\xf1\x7f\x0f\x6c\xf7\x00\xe0\x00\xff\xd1\xf5\x87"
+    b"\x0f\x8a\x0f\xff\x03\xf0\x3f\x8b\x00\x8e\x00\x90\x00\xb9\x00\x2d\xf5\xca\xf5"
+    b"\xcb\x01\x20\xf2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x50\x98\x2e"
+    b"\xd7\x0e\x50\x32\x98\x2e\xfa\x03\x00\x30\xf0\x7f\x00\x2e\x00\x2e\xd0\x2e\x00"
+    b"\x2e\x01\x80\x08\xa2\xfb\x2f\x98\x2e\xba\x03\x21\x2e\x19\x00\x01\x2e\xee\x00"
+    b"\x00\xb2\x07\x2f\x01\x2e\x19\x00\x00\xb2\x03\x2f\x01\x50\x03\x52\x98\x2e\x07"
+    b"\xcc\x01\x2e\xdd\x00\x00\xb2\x27\x2f\x05\x2e\x8a\x00\x05\x52\x98\x2e\xc7\xc1"
+    b"\x03\x2e\xe9\x00\x40\xb2\xf0\x7f\x08\x2f\x01\x2e\x19\x00\x00\xb2\x04\x2f\x00"
+    b"\x30\x21\x2e\xe9\x00\x98\x2e\xb4\xb1\x01\x2e\x18\x00\x00\xb2\x10\x2f\x05\x50"
+    b"\x98\x2e\x4d\xc3\x05\x50\x98\x2e\x5a\xc7\x98\x2e\xf9\xb4\x98\x2e\x54\xb2\x98"
+    b"\x2e\x67\xb6\x98\x2e\x17\xb2\x10\x30\x21\x2e\x77\x00\x01\x2e\xef\x00\x00\xb2"
+    b"\x04\x2f\x98\x2e\x7a\xb7\x00\x30\x21\x2e\xef\x00\x01\x2e\xd4\x00\x04\xae\x0b"
+    b"\x2f\x01\x2e\xdd\x00\x00\xb2\x07\x2f\x05\x52\x98\x2e\x8e\x0e\x00\xb2\x02\x2f"
+    b"\x10\x30\x21\x2e\x7d\x00\x01\x2e\x7d\x00\x00\x90\x90\x2e\xf1\x02\x01\x2e\xd7"
+    b"\x00\x00\xb2\x04\x2f\x98\x2e\x2f\x0e\x00\x30\x21\x2e\x7b\x00\x01\x2e\x7b\x00"
+    b"\x00\xb2\x12\x2f\x01\x2e\xd4\x00\x00\x90\x02\x2f\x98\x2e\x1f\x0e\x09\x2d\x98"
+    b"\x2e\x81\x0d\x01\x2e\xd4\x00\x04\x90\x02\x2f\x50\x32\x98\x2e\xfa\x03\x00\x30"
+    b"\x21\x2e\x7b\x00\x01\x2e\x7c\x00\x00\xb2\x90\x2e\x09\x03\x01\x2e\x7c\x00\x01"
+    b"\x31\x01\x08\x00\xb2\x04\x2f\x98\x2e\x47\xcb\x10\x30\x21\x2e\x77\x00\x81\x30"
+    b"\x01\x2e\x7c\x00\x01\x08\x00\xb2\x61\x2f\x03\x2e\x89\x00\x01\x2e\xd4\x00\x98"
+    b"\xbc\x98\xb8\x05\xb2\x0f\x58\x23\x2f\x07\x90\x09\x54\x00\x30\x37\x2f\x15\x41"
+    b"\x04\x41\xdc\xbe\x44\xbe\xdc\xba\x2c\x01\x61\x00\x0f\x56\x4a\x0f\x0c\x2f\xd1"
+    b"\x42\x94\xb8\xc1\x42\x11\x30\x05\x2e\x6a\xf7\x2c\xbd\x2f\xb9\x80\xb2\x08\x22"
+    b"\x98\x2e\xc3\xb7\x21\x2d\x61\x30\x23\x2e\xd4\x00\x98\x2e\xc3\xb7\x00\x30\x21"
+    b"\x2e\x5a\xf5\x18\x2d\xe1\x7f\x50\x30\x98\x2e\xfa\x03\x0f\x52\x07\x50\x50\x42"
+    b"\x70\x30\x0d\x54\x42\x42\x7e\x82\xe2\x6f\x80\xb2\x42\x42\x05\x2f\x21\x2e\xd4"
+    b"\x00\x10\x30\x98\x2e\xc3\xb7\x03\x2d\x60\x30\x21\x2e\xd4\x00\x01\x2e\xd4\x00"
+    b"\x06\x90\x18\x2f\x01\x2e\x76\x00\x0b\x54\x07\x52\xe0\x7f\x98\x2e\x7a\xc1\xe1"
+    b"\x6f\x08\x1a\x40\x30\x08\x2f\x21\x2e\xd4\x00\x20\x30\x98\x2e\xaf\xb7\x50\x32"
+    b"\x98\x2e\xfa\x03\x05\x2d\x98\x2e\x38\x0e\x00\x30\x21\x2e\xd4\x00\x00\x30\x21"
+    b"\x2e\x7c\x00\x18\x2d\x01\x2e\xd4\x00\x03\xaa\x01\x2f\x98\x2e\x45\x0e\x01\x2e"
+    b"\xd4\x00\x3f\x80\x03\xa2\x01\x2f\x00\x2e\x02\x2d\x98\x2e\x5b\x0e\x30\x30\x98"
+    b"\x2e\xce\xb7\x00\x30\x21\x2e\x7d\x00\x50\x32\x98\x2e\xfa\x03\x01\x2e\x77\x00"
+    b"\x00\xb2\x24\x2f\x98\x2e\xf5\xcb\x03\x2e\xd5\x00\x11\x54\x01\x0a\xbc\x84\x83"
+    b"\x86\x21\x2e\xc9\x01\xe0\x40\x13\x52\xc4\x40\x82\x40\xa8\xb9\x52\x42\x43\xbe"
+    b"\x53\x42\x04\x0a\x50\x42\xe1\x7f\xf0\x31\x41\x40\xf2\x6f\x25\xbd\x08\x08\x02"
+    b"\x0a\xd0\x7f\x98\x2e\xa8\xcf\x06\xbc\xd1\x6f\xe2\x6f\x08\x0a\x80\x42\x98\x2e"
+    b"\x58\xb7\x00\x30\x21\x2e\xee\x00\x21\x2e\x77\x00\x21\x2e\xdd\x00\x80\x2e\xf4"
+    b"\x01\x1a\x24\x22\x00\x80\x2e\xec\x01\x10\x50\xfb\x7f\x98\x2e\xf3\x03\x57\x50"
+    b"\xfb\x6f\x01\x30\x71\x54\x11\x42\x42\x0e\xfc\x2f\xc0\x2e\x01\x42\xf0\x5f\x80"
+    b"\x2e\x00\xc1\xfd\x2d\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9a\x01"
+    b"\x34\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x20\x50\xe7\x7f\xf6\x7f\x06\x32\x0f\x2e\x61\xf5\xfe\x09\xc0\xb3\x04"
+    b"\x2f\x17\x30\x2f\x2e\xef\x00\x2d\x2e\x61\xf5\xf6\x6f\xe7\x6f\xe0\x5f\xc8\x2e"
+    b"\x20\x50\xe7\x7f\xf6\x7f\x46\x30\x0f\x2e\xa4\xf1\xbe\x09\x80\xb3\x06\x2f\x0d"
+    b"\x2e\xd4\x00\x84\xaf\x02\x2f\x16\x30\x2d\x2e\x7b\x00\x86\x30\x2d\x2e\x60\xf5"
+    b"\xf6\x6f\xe7\x6f\xe0\x5f\xc8\x2e\x01\x2e\x77\xf7\x09\xbc\x0f\xb8\x00\xb2\x10"
+    b"\x50\xfb\x7f\x10\x30\x0b\x2f\x03\x2e\x8a\x00\x96\xbc\x9f\xb8\x40\xb2\x05\x2f"
+    b"\x03\x2e\x68\xf7\x9e\xbc\x9f\xb8\x40\xb2\x07\x2f\x03\x2e\x7e\x00\x41\x90\x01"
+    b"\x2f\x98\x2e\xdc\x03\x03\x2c\x00\x30\x21\x2e\x7e\x00\xfb\x6f\xf0\x5f\xb8\x2e"
+    b"\x20\x50\xe0\x7f\xfb\x7f\x00\x2e\x27\x50\x98\x2e\x3b\xc8\x29\x50\x98\x2e\xa7"
+    b"\xc8\x01\x50\x98\x2e\x55\xcc\xe1\x6f\x2b\x50\x98\x2e\xe0\xc9\xfb\x6f\x00\x30"
+    b"\xe0\x5f\x21\x2e\x7e\x00\xb8\x2e\x73\x50\x01\x30\x57\x54\x11\x42\x42\x0e\xfc"
+    b"\x2f\xb8\x2e\x21\x2e\x59\xf5\x10\x30\xc0\x2e\x21\x2e\x4a\xf1\x90\x50\xf7\x7f"
+    b"\xe6\x7f\xd5\x7f\xc4\x7f\xb3\x7f\xa1\x7f\x90\x7f\x82\x7f\x7b\x7f\x98\x2e\x35"
+    b"\xb7\x00\xb2\x90\x2e\x97\xb0\x03\x2e\x8f\x00\x07\x2e\x91\x00\x05\x2e\xb1\x00"
+    b"\x3f\xba\x9f\xb8\x01\x2e\xb1\x00\xa3\xbd\x4c\x0a\x05\x2e\xb1\x00\x04\xbe\xbf"
+    b"\xb9\xcb\x0a\x4f\xba\x22\xbd\x01\x2e\xb3\x00\xdc\x0a\x2f\xb9\x03\x2e\xb8\x00"
+    b"\x0a\xbe\x9a\x0a\xcf\xb9\x9b\xbc\x01\x2e\x97\x00\x9f\xb8\x93\x0a\x0f\xbc\x91"
+    b"\x0a\x0f\xb8\x90\x0a\x25\x2e\x18\x00\x05\x2e\xc1\xf5\x2e\xbd\x2e\xb9\x01\x2e"
+    b"\x19\x00\x31\x30\x8a\x04\x00\x90\x07\x2f\x01\x2e\xd4\x00\x04\xa2\x03\x2f\x01"
+    b"\x2e\x18\x00\x00\xb2\x0c\x2f\x19\x50\x05\x52\x98\x2e\x4d\xb7\x05\x2e\x78\x00"
+    b"\x80\x90\x10\x30\x01\x2f\x21\x2e\x78\x00\x25\x2e\xdd\x00\x98\x2e\x3e\xb7\x00"
+    b"\xb2\x02\x30\x01\x30\x04\x2f\x01\x2e\x19\x00\x00\xb2\x00\x2f\x21\x30\x01\x2e"
+    b"\xea\x00\x08\x1a\x0e\x2f\x23\x2e\xea\x00\x33\x30\x1b\x50\x0b\x09\x01\x40\x17"
+    b"\x56\x46\xbe\x4b\x08\x4c\x0a\x01\x42\x0a\x80\x15\x52\x01\x42\x00\x2e\x01\x2e"
+    b"\x18\x00\x00\xb2\x1f\x2f\x03\x2e\xc0\xf5\xf0\x30\x48\x08\x47\xaa\x74\x30\x07"
+    b"\x2e\x7a\x00\x61\x22\x4b\x1a\x05\x2f\x07\x2e\x66\xf5\xbf\xbd\xbf\xb9\xc0\x90"
+    b"\x0b\x2f\x1d\x56\x2b\x30\xd2\x42\xdb\x42\x01\x04\xc2\x42\x04\xbd\xfe\x80\x81"
+    b"\x84\x23\x2e\x7a\x00\x02\x42\x02\x32\x25\x2e\x62\xf5\x05\x2e\xd6\x00\x81\x84"
+    b"\x25\x2e\xd6\x00\x02\x31\x25\x2e\x60\xf5\x05\x2e\x8a\x00\x0b\x50\x90\x08\x80"
+    b"\xb2\x0b\x2f\x05\x2e\xca\xf5\xf0\x3e\x90\x08\x25\x2e\xca\xf5\x05\x2e\x59\xf5"
+    b"\xe0\x3f\x90\x08\x25\x2e\x59\xf5\x90\x6f\xa1\x6f\xb3\x6f\xc4\x6f\xd5\x6f\xe6"
+    b"\x6f\xf7\x6f\x7b\x6f\x82\x6f\x70\x5f\xc8\x2e\xc0\x50\x90\x7f\xe5\x7f\xd4\x7f"
+    b"\xc3\x7f\xb1\x7f\xa2\x7f\x87\x7f\xf6\x7f\x7b\x7f\x00\x2e\x01\x2e\x60\xf5\x60"
+    b"\x7f\x98\x2e\x35\xb7\x02\x30\x63\x6f\x15\x52\x50\x7f\x62\x7f\x5a\x2c\x02\x32"
+    b"\x1a\x09\x00\xb3\x14\x2f\x00\xb2\x03\x2f\x09\x2e\x18\x00\x00\x91\x0c\x2f\x43"
+    b"\x7f\x98\x2e\x97\xb7\x1f\x50\x02\x8a\x02\x32\x04\x30\x25\x2e\x64\xf5\x15\x52"
+    b"\x50\x6f\x43\x6f\x44\x43\x25\x2e\x60\xf5\xd9\x08\xc0\xb2\x36\x2f\x98\x2e\x3e"
+    b"\xb7\x00\xb2\x06\x2f\x01\x2e\x19\x00\x00\xb2\x02\x2f\x50\x6f\x00\x90\x0a\x2f"
+    b"\x01\x2e\x79\x00\x00\x90\x19\x2f\x10\x30\x21\x2e\x79\x00\x00\x30\x98\x2e\xdc"
+    b"\x03\x13\x2d\x01\x2e\xc3\xf5\x0c\xbc\x0f\xb8\x12\x30\x10\x04\x03\xb0\x26\x25"
+    b"\x21\x50\x03\x52\x98\x2e\x4d\xb7\x10\x30\x21\x2e\xee\x00\x02\x30\x60\x7f\x25"
+    b"\x2e\x79\x00\x60\x6f\x00\x90\x05\x2f\x00\x30\x21\x2e\xea\x00\x15\x50\x21\x2e"
+    b"\x64\xf5\x15\x52\x23\x2e\x60\xf5\x02\x32\x50\x6f\x00\x90\x02\x2f\x03\x30\x27"
+    b"\x2e\x78\x00\x07\x2e\x60\xf5\x1a\x09\x00\x91\xa3\x2f\x19\x09\x00\x91\xa0\x2f"
+    b"\x90\x6f\xa2\x6f\xb1\x6f\xc3\x6f\xd4\x6f\xe5\x6f\x7b\x6f\xf6\x6f\x87\x6f\x40"
+    b"\x5f\xc8\x2e\xc0\x50\xe7\x7f\xf6\x7f\x26\x30\x0f\x2e\x61\xf5\x2f\x2e\x7c\x00"
+    b"\x0f\x2e\x7c\x00\xbe\x09\xa2\x7f\x80\x7f\x80\xb3\xd5\x7f\xc4\x7f\xb3\x7f\x91"
+    b"\x7f\x7b\x7f\x0b\x2f\x23\x50\x1a\x25\x12\x40\x42\x7f\x74\x82\x12\x40\x52\x7f"
+    b"\x00\x2e\x00\x40\x60\x7f\x98\x2e\x6a\xd6\x81\x30\x01\x2e\x7c\x00\x01\x08\x00"
+    b"\xb2\x42\x2f\x03\x2e\x89\x00\x01\x2e\x89\x00\x97\xbc\x06\xbc\x9f\xb8\x0f\xb8"
+    b"\x00\x90\x23\x2e\xd8\x00\x10\x30\x01\x30\x2a\x2f\x03\x2e\xd4\x00\x44\xb2\x05"
+    b"\x2f\x47\xb2\x00\x30\x2d\x2f\x21\x2e\x7c\x00\x2b\x2d\x03\x2e\xfd\xf5\x9e\xbc"
+    b"\x9f\xb8\x40\x90\x14\x2f\x03\x2e\xfc\xf5\x99\xbc\x9f\xb8\x40\x90\x0e\x2f\x03"
+    b"\x2e\x49\xf1\x25\x54\x4a\x08\x40\x90\x08\x2f\x98\x2e\x35\xb7\x00\xb2\x10\x30"
+    b"\x03\x2f\x50\x30\x21\x2e\xd4\x00\x10\x2d\x98\x2e\xaf\xb7\x00\x30\x21\x2e\x7c"
+    b"\x00\x0a\x2d\x05\x2e\x69\xf7\x2d\xbd\x2f\xb9\x80\xb2\x01\x2f\x21\x2e\x7d\x00"
+    b"\x23\x2e\x7c\x00\xe0\x31\x21\x2e\x61\xf5\xf6\x6f\xe7\x6f\x80\x6f\xa2\x6f\xb3"
+    b"\x6f\xc4\x6f\xd5\x6f\x7b\x6f\x91\x6f\x40\x5f\xc8\x2e\x60\x51\x0a\x25\x36\x88"
+    b"\xf4\x7f\xeb\x7f\x00\x32\x31\x52\x32\x30\x13\x30\x98\x2e\x15\xcb\x0a\x25\x33"
+    b"\x84\xd2\x7f\x43\x30\x05\x50\x2d\x52\x98\x2e\x95\xc1\xd2\x6f\x27\x52\x98\x2e"
+    b"\xd7\xc7\x2a\x25\xb0\x86\xc0\x7f\xd3\x7f\xaf\x84\x29\x50\xf1\x6f\x98\x2e\x4d"
+    b"\xc8\x2a\x25\xae\x8a\xaa\x88\xf2\x6e\x2b\x50\xc1\x6f\xd3\x6f\xf4\x7f\x98\x2e"
+    b"\xb6\xc8\xe0\x6e\x00\xb2\x32\x2f\x33\x54\x83\x86\xf1\x6f\xc3\x7f\x04\x30\x30"
+    b"\x30\xf4\x7f\xd0\x7f\xb2\x7f\xe3\x30\xc5\x6f\x56\x40\x45\x41\x28\x08\x03\x14"
+    b"\x0e\xb4\x08\xbc\x82\x40\x10\x0a\x2f\x54\x26\x05\x91\x7f\x44\x28\xa3\x7f\x98"
+    b"\x2e\xd9\xc0\x08\xb9\x33\x30\x53\x09\xc1\x6f\xd3\x6f\xf4\x6f\x83\x17\x47\x40"
+    b"\x6c\x15\xb2\x6f\xbe\x09\x75\x0b\x90\x42\x45\x42\x51\x0e\x32\xbc\x02\x89\xa1"
+    b"\x6f\x7e\x86\xf4\x7f\xd0\x7f\xb2\x7f\x04\x30\x91\x6f\xd6\x2f\xeb\x6f\xa0\x5e"
+    b"\xb8\x2e\x03\x2e\x97\x00\x1b\xbc\x60\x50\x9f\xbc\x0c\xb8\xf0\x7f\x40\xb2\xeb"
+    b"\x7f\x2b\x2f\x03\x2e\x7f\x00\x41\x40\x01\x2e\xc8\x00\x01\x1a\x11\x2f\x37\x58"
+    b"\x23\x2e\xc8\x00\x10\x41\xa0\x7f\x38\x81\x01\x41\xd0\x7f\xb1\x7f\x98\x2e\x64"
+    b"\xcf\xd0\x6f\x07\x80\xa1\x6f\x11\x42\x00\x2e\xb1\x6f\x01\x42\x11\x30\x01\x2e"
+    b"\xfc\x00\x00\xa8\x03\x30\xcb\x22\x4a\x25\x01\x2e\x7f\x00\x3c\x89\x35\x52\x05"
+    b"\x54\x98\x2e\xc4\xce\xc1\x6f\xf0\x6f\x98\x2e\x95\xcf\x04\x2d\x01\x30\xf0\x6f"
+    b"\x98\x2e\x95\xcf\xeb\x6f\xa0\x5f\xb8\x2e\x03\x2e\xb3\x00\x02\x32\xf0\x30\x03"
+    b"\x31\x30\x50\x8a\x08\x08\x08\xcb\x08\xe0\x7f\x80\xb2\xf3\x7f\xdb\x7f\x25\x2f"
+    b"\x03\x2e\xca\x00\x41\x90\x04\x2f\x01\x30\x23\x2e\xca\x00\x98\x2e\x3f\x03\xc0"
+    b"\xb2\x05\x2f\x03\x2e\xda\x00\x00\x30\x41\x04\x23\x2e\xda\x00\x98\x2e\x92\xb2"
+    b"\x10\x25\xf0\x6f\x00\xb2\x05\x2f\x01\x2e\xda\x00\x02\x30\x10\x04\x21\x2e\xda"
+    b"\x00\x40\xb2\x01\x2f\x23\x2e\xc8\x01\xdb\x6f\xe0\x6f\xd0\x5f\x80\x2e\x95\xcf"
+    b"\x01\x30\xe0\x6f\x98\x2e\x95\xcf\x11\x30\x23\x2e\xca\x00\xdb\x6f\xd0\x5f\xb8"
+    b"\x2e\xd0\x50\x0a\x25\x33\x84\x55\x50\xd2\x7f\xe2\x7f\x03\x8c\xc0\x7f\xbb\x7f"
+    b"\x00\x30\x05\x5a\x39\x54\x51\x41\xa5\x7f\x96\x7f\x80\x7f\x98\x2e\xd9\xc0\x05"
+    b"\x30\xf5\x7f\x20\x25\x91\x6f\x3b\x58\x3d\x5c\x3b\x56\x98\x2e\x67\xcc\xc1\x6f"
+    b"\xd5\x6f\x52\x40\x50\x43\xc1\x7f\xd5\x7f\x10\x25\x98\x2e\xfe\xc9\x10\x25\x98"
+    b"\x2e\x74\xc0\x86\x6f\x30\x28\x92\x6f\x82\x8c\xa5\x6f\x6f\x52\x69\x0e\x39\x54"
+    b"\xdb\x2f\x19\xa0\x15\x30\x03\x2f\x00\x30\x21\x2e\x81\x01\x0a\x2d\x01\x2e\x81"
+    b"\x01\x05\x28\x42\x36\x21\x2e\x81\x01\x02\x0e\x01\x2f\x98\x2e\xf3\x03\x57\x50"
+    b"\x12\x30\x01\x40\x98\x2e\xfe\xc9\x51\x6f\x0b\x5c\x8e\x0e\x3b\x6f\x57\x58\x02"
+    b"\x30\x21\x2e\x95\x01\x45\x6f\x2a\x8d\xd2\x7f\xcb\x7f\x13\x2f\x02\x30\x3f\x50"
+    b"\xd2\x7f\xa8\x0e\x0e\x2f\xc0\x6f\x53\x54\x02\x00\x51\x54\x42\x0e\x10\x30\x59"
+    b"\x52\x02\x30\x01\x2f\x00\x2e\x03\x2d\x50\x42\x42\x42\x12\x30\xd2\x7f\x80\xb2"
+    b"\x03\x2f\x00\x30\x21\x2e\x80\x01\x12\x2d\x01\x2e\xc9\x00\x02\x80\x05\x2e\x80"
+    b"\x01\x11\x30\x91\x28\x00\x40\x25\x2e\x80\x01\x10\x0e\x05\x2f\x01\x2e\x7f\x01"
+    b"\x01\x90\x01\x2f\x98\x2e\xf3\x03\x00\x2e\xa0\x41\x01\x90\xa6\x7f\x90\x2e\xe3"
+    b"\xb4\x01\x2e\x95\x01\x00\xa8\x90\x2e\xe3\xb4\x5b\x54\x95\x80\x82\x40\x80\xb2"
+    b"\x02\x40\x2d\x8c\x3f\x52\x96\x7f\x90\x2e\xc2\xb3\x29\x0e\x76\x2f\x01\x2e\xc9"
+    b"\x00\x00\x40\x81\x28\x45\x52\xb3\x30\x98\x2e\x0f\xca\x5d\x54\x80\x7f\x00\x2e"
+    b"\xa1\x40\x72\x7f\x82\x80\x82\x40\x60\x7f\x98\x2e\xfe\xc9\x10\x25\x98\x2e\x74"
+    b"\xc0\x62\x6f\x05\x30\x87\x40\xc0\x91\x04\x30\x05\x2f\x05\x2e\x83\x01\x80\xb2"
+    b"\x14\x30\x00\x2f\x04\x30\x05\x2e\xc9\x00\x73\x6f\x81\x40\xe2\x40\x69\x04\x11"
+    b"\x0f\xe1\x40\x16\x30\xfe\x29\xcb\x40\x02\x2f\x83\x6f\x83\x0f\x22\x2f\x47\x56"
+    b"\x13\x0f\x12\x30\x77\x2f\x49\x54\x42\x0e\x12\x30\x73\x2f\x00\x91\x0a\x2f\x01"
+    b"\x2e\x8b\x01\x19\xa8\x02\x30\x6c\x2f\x63\x50\x00\x2e\x17\x42\x05\x42\x68\x2c"
+    b"\x12\x30\x0b\x25\x08\x0f\x50\x30\x02\x2f\x21\x2e\x83\x01\x03\x2d\x40\x30\x21"
+    b"\x2e\x83\x01\x2b\x2e\x85\x01\x5a\x2c\x12\x30\x00\x91\x2b\x25\x04\x2f\x63\x50"
+    b"\x02\x30\x17\x42\x17\x2c\x02\x42\x98\x2e\xfe\xc9\x10\x25\x98\x2e\x74\xc0\x05"
+    b"\x2e\xc9\x00\x81\x84\x5b\x30\x82\x40\x37\x2e\x83\x01\x02\x0e\x07\x2f\x5f\x52"
+    b"\x40\x30\x62\x40\x41\x40\x91\x0e\x01\x2f\x21\x2e\x83\x01\x05\x30\x2b\x2e\x85"
+    b"\x01\x12\x30\x36\x2c\x16\x30\x15\x25\x81\x7f\x98\x2e\xfe\xc9\x10\x25\x98\x2e"
+    b"\x74\xc0\x19\xa2\x16\x30\x15\x2f\x05\x2e\x97\x01\x80\x6f\x82\x0e\x05\x2f\x01"
+    b"\x2e\x86\x01\x06\x28\x21\x2e\x86\x01\x0b\x2d\x03\x2e\x87\x01\x5f\x54\x4e\x28"
+    b"\x91\x42\x00\x2e\x82\x40\x90\x0e\x01\x2f\x21\x2e\x88\x01\x02\x30\x13\x2c\x05"
+    b"\x30\xc0\x6f\x08\x1c\xa8\x0f\x16\x30\x05\x30\x5b\x50\x09\x2f\x02\x80\x2d\x2e"
+    b"\x82\x01\x05\x42\x05\x80\x00\x2e\x02\x42\x3e\x80\x00\x2e\x06\x42\x02\x30\x90"
+    b"\x6f\x3e\x88\x01\x40\x04\x41\x4c\x28\x01\x42\x07\x80\x10\x25\x24\x40\x00\x40"
+    b"\x00\xa8\xf5\x22\x23\x29\x44\x42\x7a\x82\x7e\x88\x43\x40\x04\x41\x00\xab\xf5"
+    b"\x23\xdf\x28\x43\x42\xd9\xa0\x14\x2f\x00\x90\x02\x2f\xd2\x6f\x81\xb2\x05\x2f"
+    b"\x63\x54\x06\x28\x90\x42\x85\x42\x09\x2c\x02\x30\x5b\x50\x03\x80\x29\x2e\x7e"
+    b"\x01\x2b\x2e\x82\x01\x05\x42\x12\x30\x2b\x2e\x83\x01\x45\x82\x00\x2e\x40\x40"
+    b"\x7a\x82\x02\xa0\x08\x2f\x63\x50\x3b\x30\x15\x42\x05\x42\x37\x80\x37\x2e\x7e"
+    b"\x01\x05\x42\x12\x30\x01\x2e\xc9\x00\x02\x8c\x40\x40\x84\x41\x7a\x8c\x04\x0f"
+    b"\x03\x2f\x01\x2e\x8b\x01\x19\xa4\x04\x2f\x2b\x2e\x82\x01\x98\x2e\xf3\x03\x12"
+    b"\x30\x81\x90\x61\x52\x08\x2f\x65\x42\x65\x42\x43\x80\x39\x84\x82\x88\x05\x42"
+    b"\x45\x42\x85\x42\x05\x43\x00\x2e\x80\x41\x00\x90\x90\x2e\xe1\xb4\x65\x54\xc1"
+    b"\x6f\x80\x40\x00\xb2\x43\x58\x69\x50\x44\x2f\x55\x5c\xb7\x87\x8c\x0f\x0d\x2e"
+    b"\x96\x01\xc4\x40\x36\x2f\x41\x56\x8b\x0e\x2a\x2f\x0b\x52\xa1\x0e\x0a\x2f\x05"
+    b"\x2e\x8f\x01\x14\x25\x98\x2e\xfe\xc9\x4b\x54\x02\x0f\x69\x50\x05\x30\x65\x54"
+    b"\x15\x2f\x03\x2e\x8e\x01\x4d\x5c\x8e\x0f\x3a\x2f\x05\x2e\x8f\x01\x98\x2e\xfe"
+    b"\xc9\x4f\x54\x82\x0f\x05\x30\x69\x50\x65\x54\x30\x2f\x6d\x52\x15\x30\x42\x8c"
+    b"\x45\x42\x04\x30\x2b\x2c\x84\x43\x6b\x52\x42\x8c\x00\x2e\x85\x43\x15\x30\x24"
+    b"\x2c\x45\x42\x8e\x0f\x20\x2f\x0d\x2e\x8e\x01\xb1\x0e\x1c\x2f\x23\x2e\x8e\x01"
+    b"\x1a\x2d\x0e\x0e\x17\x2f\xa1\x0f\x15\x2f\x23\x2e\x8d\x01\x13\x2d\x98\x2e\x74"
+    b"\xc0\x43\x54\xc2\x0e\x0a\x2f\x65\x50\x04\x80\x0b\x30\x06\x82\x0b\x42\x79\x80"
+    b"\x41\x40\x12\x30\x25\x2e\x8c\x01\x01\x42\x05\x30\x69\x50\x65\x54\x84\x82\x43"
+    b"\x84\xbe\x8c\x84\x40\x86\x41\x26\x29\x94\x42\xbe\x8e\xd5\x7f\x19\xa1\x43\x40"
+    b"\x0b\x2e\x8c\x01\x84\x40\xc7\x41\x5d\x29\x27\x29\x45\x42\x84\x42\xc2\x7f\x01"
+    b"\x2f\xc0\xb3\x1d\x2f\x05\x2e\x94\x01\x99\xa0\x01\x2f\x80\xb3\x13\x2f\x80\xb3"
+    b"\x18\x2f\xc0\xb3\x16\x2f\x12\x40\x01\x40\x92\x7f\x98\x2e\x74\xc0\x92\x6f\x10"
+    b"\x0f\x20\x30\x03\x2f\x10\x30\x21\x2e\x7e\x01\x0a\x2d\x21\x2e\x7e\x01\x07\x2d"
+    b"\x20\x30\x21\x2e\x7e\x01\x03\x2d\x10\x30\x21\x2e\x7e\x01\xc2\x6f\x01\x2e\xc9"
+    b"\x00\xbc\x84\x02\x80\x82\x40\x00\x40\x90\x0e\xd5\x6f\x02\x2f\x15\x30\x98\x2e"
+    b"\xf3\x03\x41\x91\x05\x30\x07\x2f\x67\x50\x3d\x80\x2b\x2e\x8f\x01\x05\x42\x04"
+    b"\x80\x00\x2e\x05\x42\x02\x2c\x00\x30\x00\x30\xa2\x6f\x98\x8a\x86\x40\x80\xa7"
+    b"\x05\x2f\x98\x2e\xf3\x03\xc0\x30\x21\x2e\x95\x01\x06\x25\x1a\x25\xe2\x6f\x76"
+    b"\x82\x96\x40\x56\x43\x51\x0e\xfb\x2f\xbb\x6f\x30\x5f\xb8\x2e\x01\x2e\xb8\x00"
+    b"\x01\x31\x41\x08\x40\xb2\x20\x50\xf2\x30\x02\x08\xfb\x7f\x01\x30\x10\x2f\x05"
+    b"\x2e\xcc\x00\x81\x90\xe0\x7f\x03\x2f\x23\x2e\xcc\x00\x98\x2e\x55\xb6\x98\x2e"
+    b"\x1d\xb5\x10\x25\xfb\x6f\xe0\x6f\xe0\x5f\x80\x2e\x95\xcf\x98\x2e\x95\xcf\x10"
+    b"\x30\x21\x2e\xcc\x00\xfb\x6f\xe0\x5f\xb8\x2e\x00\x51\x05\x58\xeb\x7f\x2a\x25"
+    b"\x89\x52\x6f\x5a\x89\x50\x13\x41\x06\x40\xb3\x01\x16\x42\xcb\x16\x06\x40\xf3"
+    b"\x02\x13\x42\x65\x0e\xf5\x2f\x05\x40\x14\x30\x2c\x29\x04\x42\x08\xa1\x00\x30"
+    b"\x90\x2e\x52\xb6\xb3\x88\xb0\x8a\xb6\x84\xa4\x7f\xc4\x7f\xb5\x7f\xd5\x7f\x92"
+    b"\x7f\x73\x30\x04\x30\x55\x40\x42\x40\x8a\x17\xf3\x08\x6b\x01\x90\x02\x53\xb8"
+    b"\x4b\x82\xad\xbe\x71\x7f\x45\x0a\x09\x54\x84\x7f\x98\x2e\xd9\xc0\xa3\x6f\x7b"
+    b"\x54\xd0\x42\xa3\x7f\xf2\x7f\x60\x7f\x20\x25\x71\x6f\x75\x5a\x77\x58\x79\x5c"
+    b"\x75\x56\x98\x2e\x67\xcc\xb1\x6f\x62\x6f\x50\x42\xb1\x7f\xb3\x30\x10\x25\x98"
+    b"\x2e\x0f\xca\x84\x6f\x20\x29\x71\x6f\x92\x6f\xa5\x6f\x76\x82\x6a\x0e\x73\x30"
+    b"\x00\x30\xd0\x2f\xd2\x6f\xd1\x7f\xb4\x7f\x98\x2e\x2b\xb7\x15\xbd\x0b\xb8\x02"
+    b"\x0a\xc2\x6f\xc0\x7f\x98\x2e\x2b\xb7\x15\xbd\x0b\xb8\x42\x0a\xc0\x6f\x08\x17"
+    b"\x41\x18\x89\x16\xe1\x18\xd0\x18\xa1\x7f\x27\x25\x16\x25\x98\x2e\x79\xc0\x8b"
+    b"\x54\x90\x7f\xb3\x30\x82\x40\x80\x90\x0d\x2f\x7d\x52\x92\x6f\x98\x2e\x0f\xca"
+    b"\xb2\x6f\x90\x0e\x06\x2f\x8b\x50\x14\x30\x42\x6f\x51\x6f\x14\x42\x12\x42\x01"
+    b"\x42\x00\x2e\x31\x6f\x98\x2e\x74\xc0\x41\x6f\x80\x7f\x98\x2e\x74\xc0\x82\x6f"
+    b"\x10\x04\x43\x52\x01\x0f\x05\x2e\xcb\x00\x00\x30\x04\x30\x21\x2f\x51\x6f\x43"
+    b"\x58\x8c\x0e\x04\x30\x1c\x2f\x85\x88\x41\x6f\x04\x41\x8c\x0f\x04\x30\x16\x2f"
+    b"\x84\x88\x00\x2e\x04\x41\x04\x05\x8c\x0e\x04\x30\x0f\x2f\x82\x88\x31\x6f\x04"
+    b"\x41\x04\x05\x8c\x0e\x04\x30\x08\x2f\x83\x88\x00\x2e\x04\x41\x8c\x0f\x04\x30"
+    b"\x02\x2f\x21\x2e\xad\x01\x14\x30\x00\x91\x14\x2f\x03\x2e\xa1\x01\x41\x90\x0e"
+    b"\x2f\x03\x2e\xad\x01\x14\x30\x4c\x28\x23\x2e\xad\x01\x46\xa0\x06\x2f\x81\x84"
+    b"\x8d\x52\x48\x82\x82\x40\x21\x2e\xa1\x01\x42\x42\x5c\x2c\x02\x30\x05\x2e\xaa"
+    b"\x01\x80\xb2\x02\x30\x55\x2f\x03\x2e\xa9\x01\x92\x6f\xb3\x30\x98\x2e\x0f\xca"
+    b"\xb2\x6f\x90\x0f\x00\x30\x02\x30\x4a\x2f\xa2\x6f\x87\x52\x91\x00\x85\x52\x51"
+    b"\x0e\x02\x2f\x00\x2e\x43\x2c\x02\x30\xc2\x6f\x7f\x52\x91\x0e\x02\x30\x3c\x2f"
+    b"\x51\x6f\x81\x54\x98\x2e\xfe\xc9\x10\x25\xb3\x30\x21\x25\x98\x2e\x0f\xca\x32"
+    b"\x6f\xc0\x7f\xb3\x30\x12\x25\x98\x2e\x0f\xca\x42\x6f\xb0\x7f\xb3\x30\x12\x25"
+    b"\x98\x2e\x0f\xca\xb2\x6f\x90\x28\x83\x52\x98\x2e\xfe\xc9\xc2\x6f\x90\x0f\x00"
+    b"\x30\x02\x30\x1d\x2f\x05\x2e\xa1\x01\x80\xb2\x12\x30\x0f\x2f\x42\x6f\x03\x2e"
+    b"\xab\x01\x91\x0e\x02\x30\x12\x2f\x52\x6f\x03\x2e\xac\x01\x91\x0f\x02\x30\x0c"
+    b"\x2f\x21\x2e\xaa\x01\x0a\x2c\x12\x30\x03\x2e\xcb\x00\x8d\x58\x08\x89\x41\x40"
+    b"\x11\x43\x00\x43\x25\x2e\xa1\x01\xd4\x6f\x8f\x52\x00\x43\x3a\x89\x00\x2e\x10"
+    b"\x43\x10\x43\x61\x0e\xfb\x2f\x03\x2e\xa0\x01\x11\x1a\x02\x2f\x02\x25\x21\x2e"
+    b"\xa0\x01\xeb\x6f\x00\x5f\xb8\x2e\x91\x52\x10\x30\x02\x30\x95\x56\x52\x42\x4b"
+    b"\x0e\xfc\x2f\x8d\x54\x88\x82\x93\x56\x80\x42\x53\x42\x40\x42\x42\x86\x83\x54"
+    b"\xc0\x2e\xc2\x42\x00\x2e\xa3\x52\x00\x51\x52\x40\x47\x40\x1a\x25\x01\x2e\x97"
+    b"\x00\x8f\xbe\x72\x86\xfb\x7f\x0b\x30\x7c\xbf\xa5\x50\x10\x08\xdf\xba\x70\x88"
+    b"\xf8\xbf\xcb\x42\xd3\x7f\x6c\xbb\xfc\xbb\xc5\x0a\x90\x7f\x1b\x7f\x0b\x43\xc0"
+    b"\xb2\xe5\x7f\xb7\x7f\xa6\x7f\xc4\x7f\x90\x2e\x1c\xb7\x07\x2e\xd2\x00\xc0\xb2"
+    b"\x0b\x2f\x97\x52\x01\x2e\xcd\x00\x82\x7f\x98\x2e\xbb\xcc\x0b\x30\x37\x2e\xd2"
+    b"\x00\x82\x6f\x90\x6f\x1a\x25\x00\xb2\x8b\x7f\x14\x2f\xa6\xbd\x25\xbd\xb6\xb9"
+    b"\x2f\xb9\x80\xb2\xd4\xb0\x0c\x2f\x99\x54\x9b\x56\x0b\x30\x0b\x2e\xb1\x00\xa1"
+    b"\x58\x9b\x42\xdb\x42\x6c\x09\x2b\x2e\xb1\x00\x8b\x42\xcb\x42\x86\x7f\x73\x84"
+    b"\xa7\x56\xc3\x08\x39\x52\x05\x50\x72\x7f\x63\x7f\x98\x2e\xc2\xc0\xe1\x6f\x62"
+    b"\x6f\xd1\x0a\x01\x2e\xcd\x00\xd5\x6f\xc4\x6f\x72\x6f\x97\x52\x9d\x5c\x98\x2e"
+    b"\x06\xcd\x23\x6f\x90\x6f\x99\x52\xc0\xb2\x04\xbd\x54\x40\xaf\xb9\x45\x40\xe1"
+    b"\x7f\x02\x30\x06\x2f\xc0\xb2\x02\x30\x03\x2f\x9b\x5c\x12\x30\x94\x43\x85\x43"
+    b"\x03\xbf\x6f\xbb\x80\xb3\x20\x2f\x06\x6f\x26\x01\x16\x6f\x6e\x03\x45\x42\xc0"
+    b"\x90\x29\x2e\xce\x00\x9b\x52\x14\x2f\x9b\x5c\x00\x2e\x93\x41\x86\x41\xe3\x04"
+    b"\xae\x07\x80\xab\x04\x2f\x80\x91\x0a\x2f\x86\x6f\x73\x0f\x07\x2f\x83\x6f\xc0"
+    b"\xb2\x04\x2f\x54\x42\x45\x42\x12\x30\x04\x2c\x11\x30\x02\x2c\x11\x30\x11\x30"
+    b"\x02\xbc\x0f\xb8\xd2\x7f\x00\xb2\x0a\x2f\x01\x2e\xfc\x00\x05\x2e\xc7\x01\x10"
+    b"\x1a\x02\x2f\x21\x2e\xc7\x01\x03\x2d\x02\x2c\x01\x30\x01\x30\xb0\x6f\x98\x2e"
+    b"\x95\xcf\xd1\x6f\xa0\x6f\x98\x2e\x95\xcf\xe2\x6f\x9f\x52\x01\x2e\xce\x00\x82"
+    b"\x40\x50\x42\x0c\x2c\x42\x42\x11\x30\x23\x2e\xd2\x00\x01\x30\xb0\x6f\x98\x2e"
+    b"\x95\xcf\xa0\x6f\x01\x30\x98\x2e\x95\xcf\x00\x2e\xfb\x6f\x00\x5f\xb8\x2e\x83"
+    b"\x86\x01\x30\x00\x30\x94\x40\x24\x18\x06\x00\x53\x0e\x4f\x02\xf9\x2f\xb8\x2e"
+    b"\xa9\x52\x00\x2e\x60\x40\x41\x40\x0d\xbc\x98\xbc\xc0\x2e\x01\x0a\x0f\xb8\xab"
+    b"\x52\x53\x3c\x52\x40\x40\x40\x4b\x00\x82\x16\x26\xb9\x01\xb8\x41\x40\x10\x08"
+    b"\x97\xb8\x01\x08\xc0\x2e\x11\x30\x01\x08\x43\x86\x25\x40\x04\x40\xd8\xbe\x2c"
+    b"\x0b\x22\x11\x54\x42\x03\x80\x4b\x0e\xf6\x2f\xb8\x2e\x9f\x50\x10\x50\xad\x52"
+    b"\x05\x2e\xd3\x00\xfb\x7f\x00\x2e\x13\x40\x93\x42\x41\x0e\xfb\x2f\x98\x2e\xa5"
+    b"\xb7\x98\x2e\x87\xcf\x01\x2e\xd9\x00\x00\xb2\xfb\x6f\x0b\x2f\x01\x2e\x69\xf7"
+    b"\xb1\x3f\x01\x08\x01\x30\xf0\x5f\x23\x2e\xd9\x00\x21\x2e\x69\xf7\x80\x2e\x7a"
+    b"\xb7\xf0\x5f\xb8\x2e\x01\x2e\xc0\xf8\x03\x2e\xfc\xf5\x15\x54\xaf\x56\x82\x08"
+    b"\x0b\x2e\x69\xf7\xcb\x0a\xb1\x58\x80\x90\xdd\xbe\x4c\x08\x5f\xb9\x59\x22\x80"
+    b"\x90\x07\x2f\x03\x34\xc3\x08\xf2\x3a\x0a\x08\x02\x35\xc0\x90\x4a\x0a\x48\x22"
+    b"\xc0\x2e\x23\x2e\xfc\xf5\x10\x50\xfb\x7f\x98\x2e\x56\xc7\x98\x2e\x49\xc3\x10"
+    b"\x30\xfb\x6f\xf0\x5f\x21\x2e\xcc\x00\x21\x2e\xca\x00\xb8\x2e\x03\x2e\xd3\x00"
+    b"\x16\xb8\x02\x34\x4a\x0c\x21\x2e\x2d\xf5\xc0\x2e\x23\x2e\xd3\x00\x03\xbc\x21"
+    b"\x2e\xd5\x00\x03\x2e\xd5\x00\x40\xb2\x10\x30\x21\x2e\x77\x00\x01\x30\x05\x2f"
+    b"\x05\x2e\xd8\x00\x80\x90\x01\x2f\x23\x2e\x6f\xf5\xc0\x2e\x21\x2e\xd9\x00\x11"
+    b"\x30\x81\x08\x01\x2e\x6a\xf7\x71\x3f\x23\xbd\x01\x08\x02\x0a\xc0\x2e\x21\x2e"
+    b"\x6a\xf7\x30\x25\x00\x30\x21\x2e\x5a\xf5\x10\x50\x21\x2e\x7b\x00\x21\x2e\x7c"
+    b"\x00\xfb\x7f\x98\x2e\xc3\xb7\x40\x30\x21\x2e\xd4\x00\xfb\x6f\xf0\x5f\x03\x25"
+    b"\x80\x2e\xaf\xb7\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00"
+    b"\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e"
+    b"\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80"
+    b"\x2e\x00\xc1\x80\x2e\x00\xc1\x01\x2e\x5d\xf7\x08\xbc\x80\xac\x0e\xbb\x02\x2f"
+    b"\x00\x30\x41\x04\x82\x06\xc0\xa4\x00\x30\x11\x2f\x40\xa9\x03\x2f\x40\x91\x0d"
+    b"\x2f\x00\xa7\x0b\x2f\x80\xb3\xb3\x58\x02\x2f\x90\xa1\x26\x13\x20\x23\x80\x90"
+    b"\x10\x30\x01\x2f\xcc\x0e\x00\x2f\x00\x30\xb8\x2e\xb5\x50\x18\x08\x08\xbc\x88"
+    b"\xb6\x0d\x17\xc6\xbd\x56\xbc\xb7\x58\xda\xba\x04\x01\x1d\x0a\x10\x50\x05\x30"
+    b"\x32\x25\x45\x03\xfb\x7f\xf6\x30\x21\x25\x98\x2e\x37\xca\x16\xb5\x9a\xbc\x06"
+    b"\xb8\x80\xa8\x41\x0a\x0e\x2f\x80\x90\x02\x2f\x2d\x50\x48\x0f\x09\x2f\xbf\xa0"
+    b"\x04\x2f\xbf\x90\x06\x2f\xb7\x54\xca\x0f\x03\x2f\x00\x2e\x02\x2c\xb7\x52\x2d"
+    b"\x52\xf2\x33\x98\x2e\xd9\xc0\xfb\x6f\xf1\x37\xc0\x2e\x01\x08\xf0\x5f\xbf\x56"
+    b"\xb9\x54\xd0\x40\xc4\x40\x0b\x2e\xfd\xf3\xbf\x52\x90\x42\x94\x42\x95\x42\x05"
+    b"\x30\xc1\x50\x0f\x88\x06\x40\x04\x41\x96\x42\xc5\x42\x48\xbe\x73\x30\x0d\x2e"
+    b"\xd8\x00\x4f\xba\x84\x42\x03\x42\x81\xb3\x02\x2f\x2b\x2e\x6f\xf5\x06\x2d\x05"
+    b"\x2e\x77\xf7\xbd\x56\x93\x08\x25\x2e\x77\xf7\xbb\x54\x25\x2e\xc2\xf5\x07\x2e"
+    b"\xfd\xf3\x42\x30\xb4\x33\xda\x0a\x4c\x00\x27\x2e\xfd\xf3\x43\x40\xd4\x3f\xdc"
+    b"\x08\x43\x42\x00\x2e\x00\x2e\x43\x40\x24\x30\xdc\x0a\x43\x42\x04\x80\x03\x2e"
+    b"\xfd\xf3\x4a\x0a\x23\x2e\xfd\xf3\x61\x34\xc0\x2e\x01\x42\x00\x2e\x60\x50\x1a"
+    b"\x25\x7a\x86\xe0\x7f\xf3\x7f\x03\x25\xc3\x52\x41\x84\xdb\x7f\x33\x30\x98\x2e"
+    b"\x16\xc2\x1a\x25\x7d\x82\xf0\x6f\xe2\x6f\x32\x25\x16\x40\x94\x40\x26\x01\x85"
+    b"\x40\x8e\x17\xc4\x42\x6e\x03\x95\x42\x41\x0e\xf4\x2f\xdb\x6f\xa0\x5f\xb8\x2e"
+    b"\xb0\x51\xfb\x7f\x98\x2e\xe8\x0d\x5a\x25\x98\x2e\x0f\x0e\xcb\x58\x32\x87\xc4"
+    b"\x7f\x65\x89\x6b\x8d\xc5\x5a\x65\x7f\xe1\x7f\x83\x7f\xa6\x7f\x74\x7f\xd0\x7f"
+    b"\xb6\x7f\x94\x7f\x17\x30\xc7\x52\xc9\x54\x51\x7f\x00\x2e\x85\x6f\x42\x7f\x00"
+    b"\x2e\x51\x41\x45\x81\x42\x41\x13\x40\x3b\x8a\x00\x40\x4b\x04\xd0\x06\xc0\xac"
+    b"\x85\x7f\x02\x2f\x02\x30\x51\x04\xd3\x06\x41\x84\x05\x30\x5d\x02\xc9\x16\xdf"
+    b"\x08\xd3\x00\x8d\x02\xaf\xbc\xb1\xb9\x59\x0a\x65\x6f\x11\x43\xa1\xb4\x52\x41"
+    b"\x53\x41\x01\x43\x34\x7f\x65\x7f\x26\x31\xe5\x6f\xd4\x6f\x98\x2e\x37\xca\x32"
+    b"\x6f\x75\x6f\x83\x40\x42\x41\x23\x7f\x12\x7f\xf6\x30\x40\x25\x51\x25\x98\x2e"
+    b"\x37\xca\x14\x6f\x20\x05\x70\x6f\x25\x6f\x69\x07\xa2\x6f\x31\x6f\x0b\x30\x04"
+    b"\x42\x9b\x42\x8b\x42\x55\x42\x32\x7f\x40\xa9\xc3\x6f\x71\x7f\x02\x30\xd0\x40"
+    b"\xc3\x7f\x03\x2f\x40\x91\x15\x2f\x00\xa7\x13\x2f\x00\xa4\x11\x2f\x84\xbd\x98"
+    b"\x2e\x79\xca\x55\x6f\xb7\x54\x54\x41\x82\x00\xf3\x3f\x45\x41\xcb\x02\xf6\x30"
+    b"\x98\x2e\x37\xca\x35\x6f\xa4\x6f\x41\x43\x03\x2c\x00\x43\xa4\x6f\x35\x6f\x17"
+    b"\x30\x42\x6f\x51\x6f\x93\x40\x42\x82\x00\x41\xc3\x00\x03\x43\x51\x7f\x00\x2e"
+    b"\x94\x40\x41\x41\x4c\x02\xc4\x6f\xd1\x56\x63\x0e\x74\x6f\x51\x43\xa5\x7f\x8a"
+    b"\x2f\x09\x2e\xd8\x00\x01\xb3\x21\x2f\xcb\x58\x90\x6f\x13\x41\xb6\x6f\xe4\x7f"
+    b"\x00\x2e\x91\x41\x14\x40\x92\x41\x15\x40\x17\x2e\x6f\xf5\xb6\x7f\xd0\x7f\xcb"
+    b"\x7f\x98\x2e\x00\x0c\x07\x15\xc2\x6f\x14\x0b\x29\x2e\x6f\xf5\xc3\xa3\xc1\x8f"
+    b"\xe4\x6f\xd0\x6f\xe6\x2f\x14\x30\x05\x2e\x6f\xf5\x14\x0b\x29\x2e\x6f\xf5\x18"
+    b"\x2d\xcd\x56\x04\x32\xb5\x6f\x1c\x01\x51\x41\x52\x41\xc3\x40\xb5\x7f\xe4\x7f"
+    b"\x98\x2e\x1f\x0c\xe4\x6f\x21\x87\x00\x43\x04\x32\xcf\x54\x5a\x0e\xef\x2f\x15"
+    b"\x54\x09\x2e\x77\xf7\x22\x0b\x29\x2e\x77\xf7\xfb\x6f\x50\x5e\xb8\x2e\x10\x50"
+    b"\x01\x2e\xd4\x00\x00\xb2\xfb\x7f\x51\x2f\x01\xb2\x48\x2f\x02\xb2\x42\x2f\x03"
+    b"\x90\x56\x2f\xd7\x52\x79\x80\x42\x40\x81\x84\x00\x40\x42\x42\x98\x2e\x93\x0c"
+    b"\xd9\x54\xd7\x50\xa1\x40\x98\xbd\x82\x40\x3e\x82\xda\x0a\x44\x40\x8b\x16\xe3"
+    b"\x00\x53\x42\x00\x2e\x43\x40\x9a\x02\x52\x42\x00\x2e\x41\x40\x15\x54\x4a\x0e"
+    b"\x3a\x2f\x3a\x82\x00\x30\x41\x40\x21\x2e\x85\x0f\x40\xb2\x0a\x2f\x98\x2e\xb1"
+    b"\x0c\x98\x2e\x45\x0e\x98\x2e\x5b\x0e\xfb\x6f\xf0\x5f\x00\x30\x80\x2e\xce\xb7"
+    b"\xdd\x52\xd3\x54\x42\x42\x4f\x84\x73\x30\xdb\x52\x83\x42\x1b\x30\x6b\x42\x23"
+    b"\x30\x27\x2e\xd7\x00\x37\x2e\xd4\x00\x21\x2e\xd6\x00\x7a\x84\x17\x2c\x42\x42"
+    b"\x30\x30\x21\x2e\xd4\x00\x12\x2d\x21\x30\x00\x30\x23\x2e\xd4\x00\x21\x2e\x7b"
+    b"\xf7\x0b\x2d\x17\x30\x98\x2e\x51\x0c\xd5\x50\x0c\x82\x72\x30\x2f\x2e\xd4\x00"
+    b"\x25\x2e\x7b\xf7\x40\x42\x00\x2e\xfb\x6f\xf0\x5f\xb8\x2e\x70\x50\x0a\x25\x39"
+    b"\x86\xfb\x7f\xe1\x32\x62\x30\x98\x2e\xc2\xc4\xb5\x56\xa5\x6f\xab\x08\x91\x6f"
+    b"\x4b\x08\xdf\x56\xc4\x6f\x23\x09\x4d\xba\x93\xbc\x8c\x0b\xd1\x6f\x0b\x09\xcb"
+    b"\x52\xe1\x5e\x56\x42\xaf\x09\x4d\xba\x23\xbd\x94\x0a\xe5\x6f\x68\xbb\xeb\x08"
+    b"\xbd\xb9\x63\xbe\xfb\x6f\x52\x42\xe3\x0a\xc0\x2e\x43\x42\x90\x5f\xd1\x50\x03"
+    b"\x2e\x25\xf3\x13\x40\x00\x40\x9b\xbc\x9b\xb4\x08\xbd\xb8\xb9\x98\xbc\xda\x0a"
+    b"\x08\xb6\x89\x16\xc0\x2e\x19\x00\x62\x02\x10\x50\xfb\x7f\x98\x2e\x81\x0d\x01"
+    b"\x2e\xd4\x00\x31\x30\x08\x04\xfb\x6f\x01\x30\xf0\x5f\x23\x2e\xd6\x00\x21\x2e"
+    b"\xd7\x00\xb8\x2e\x01\x2e\xd7\x00\x03\x2e\xd6\x00\x48\x0e\x01\x2f\x80\x2e\x1f"
+    b"\x0e\xb8\x2e\xe3\x50\x21\x34\x01\x42\x82\x30\xc1\x32\x25\x2e\x62\xf5\x01\x00"
+    b"\x22\x30\x01\x40\x4a\x0a\x01\x42\xb8\x2e\xe3\x54\xf0\x3b\x83\x40\xd8\x08\xe5"
+    b"\x52\x83\x42\x00\x30\x83\x30\x50\x42\xc4\x32\x27\x2e\x64\xf5\x94\x00\x50\x42"
+    b"\x40\x42\xd3\x3f\x84\x40\x7d\x82\xe3\x08\x40\x42\x83\x42\xb8\x2e\xdd\x52\x00"
+    b"\x30\x40\x42\x7c\x86\xb9\x52\x09\x2e\x70\x0f\xbf\x54\xc4\x42\xd3\x86\x54\x40"
+    b"\x55\x40\x94\x42\x85\x42\x21\x2e\xd7\x00\x42\x40\x25\x2e\xfd\xf3\xc0\x42\x7e"
+    b"\x82\x05\x2e\x7d\x00\x80\xb2\x14\x2f\x05\x2e\x89\x00\x27\xbd\x2f\xb9\x80\x90"
+    b"\x02\x2f\x21\x2e\x6f\xf5\x0c\x2d\x07\x2e\x71\x0f\x14\x30\x1c\x09\x05\x2e\x77"
+    b"\xf7\xbd\x56\x47\xbe\x93\x08\x94\x0a\x25\x2e\x77\xf7\xe7\x54\x50\x42\x4a\x0e"
+    b"\xfc\x2f\xb8\x2e\x50\x50\x02\x30\x43\x86\xe5\x50\xfb\x7f\xe3\x7f\xd2\x7f\xc0"
+    b"\x7f\xb1\x7f\x00\x2e\x41\x40\x00\x40\x48\x04\x98\x2e\x74\xc0\x1e\xaa\xd3\x6f"
+    b"\x14\x30\xb1\x6f\xe3\x22\xc0\x6f\x52\x40\xe4\x6f\x4c\x0e\x12\x42\xd3\x7f\xeb"
+    b"\x2f\x03\x2e\x86\x0f\x40\x90\x11\x30\x03\x2f\x23\x2e\x86\x0f\x02\x2c\x00\x30"
+    b"\xd0\x6f\xfb\x6f\xb0\x5f\xb8\x2e\x40\x50\xf1\x7f\x0a\x25\x3c\x86\xeb\x7f\x41"
+    b"\x33\x22\x30\x98\x2e\xc2\xc4\xd3\x6f\xf4\x30\xdc\x09\x47\x58\xc2\x6f\x94\x09"
+    b"\xeb\x58\x6a\xbb\xdc\x08\xb4\xb9\xb1\xbd\xe9\x5a\x95\x08\x21\xbd\xf6\xbf\x77"
+    b"\x0b\x51\xbe\xf1\x6f\xeb\x6f\x52\x42\x54\x42\xc0\x2e\x43\x42\xc0\x5f\x50\x50"
+    b"\xf5\x50\x31\x30\x11\x42\xfb\x7f\x7b\x30\x0b\x42\x11\x30\x02\x80\x23\x33\x01"
+    b"\x42\x03\x00\x07\x2e\x80\x03\x05\x2e\xd3\x00\x23\x52\xe2\x7f\xd3\x7f\xc0\x7f"
+    b"\x98\x2e\xb6\x0e\xd1\x6f\x08\x0a\x1a\x25\x7b\x86\xd0\x7f\x01\x33\x12\x30\x98"
+    b"\x2e\xc2\xc4\xd1\x6f\x08\x0a\x00\xb2\x0d\x2f\xe3\x6f\x01\x2e\x80\x03\x51\x30"
+    b"\xc7\x86\x23\x2e\x21\xf2\x08\xbc\xc0\x42\x98\x2e\xa5\xb7\x00\x2e\x00\x2e\xd0"
+    b"\x2e\xb0\x6f\x0b\xb8\x03\x2e\x1b\x00\x08\x1a\xb0\x7f\x70\x30\x04\x2f\x21\x2e"
+    b"\x21\xf2\x00\x2e\x00\x2e\xd0\x2e\x98\x2e\x6d\xc0\x98\x2e\x5d\xc0\xed\x50\x98"
+    b"\x2e\x44\xcb\xef\x50\x98\x2e\x46\xc3\xf1\x50\x98\x2e\x53\xc7\x35\x50\x98\x2e"
+    b"\x64\xcf\x10\x30\x98\x2e\xdc\x03\x20\x26\xc0\x6f\x02\x31\x12\x42\xab\x33\x0b"
+    b"\x42\x37\x80\x01\x30\x01\x42\xf3\x37\xf7\x52\xfb\x50\x44\x40\xa2\x0a\x42\x42"
+    b"\x8b\x31\x09\x2e\x5e\xf7\xf9\x54\xe3\x08\x83\x42\x1b\x42\x23\x33\x4b\x00\xbc"
+    b"\x84\x0b\x40\x33\x30\x83\x42\x0b\x42\xe0\x7f\xd1\x7f\x98\x2e\x58\xb7\xd1\x6f"
+    b"\x80\x30\x40\x42\x03\x30\xe0\x6f\xf3\x54\x04\x30\x00\x2e\x00\x2e\x01\x89\x62"
+    b"\x0e\xfa\x2f\x43\x42\x11\x30\xfb\x6f\xc0\x2e\x01\x42\xb0\x5f\xc1\x4a\x00\x00"
+    b"\x6d\x57\x00\x00\x77\x8e\x00\x00\xe0\xff\xff\xff\xd3\xff\xff\xff\xe5\xff\xff"
+    b"\xff\xee\xe1\xff\xff\x7c\x13\x00\x00\x46\xe6\xff\xff\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x2e\x00\xc1\x80"
+    b"\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1"
+    b"\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00"
+    b"\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e"
+    b"\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80"
+    b"\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1"
+    b"\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00"
+    b"\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e"
+    b"\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80"
+    b"\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1"
+    b"\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00"
+    b"\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e"
+    b"\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80\x2e\x00\xc1\x80"
+    b"\x2e\x00\xc1"
+)
+
+
+class BMI270:
+    def __init__(
+        self,
+        bus,
+        cs=None,
+        address=_DEFAULT_ADDR,
+        gyro_odr=100,
+        gyro_scale=2000,
+        accel_odr=100,
+        accel_scale=4,
+        bmm_magnet=None,
+    ):
+        """Initalizes Gyro and Accelerometer.
+        bus: IMU bus
+        address: I2C address (in I2C mode).
+        cs: SPI CS pin (in SPI mode).
+        gyro_odr:  (0.78, 1.5Hz, 3.1Hz, 6.25Hz, 12.5Hz, 25Hz, 50Hz, 100Hz, 200Hz, 400Hz, 800Hz, 1600Hz)
+        gyro_scale:  (125dps, 250dps, 500dps, 1000dps, 2000dps)
+        accel_odr: (0.78, 1.5Hz, 3.1Hz, 6.25Hz, 12.5Hz, 25Hz, 50Hz, 100Hz, 200Hz, 400Hz, 800Hz, 1600Hz)
+        accel_scale: (+/-2g, +/-4g, +/-8g, +-16g)
+        """
+        self.bus = bus
+        self.bmm_magnet = bmm_magnet
+        self.cs = cs
+        self.address = address
+        self._use_i2c = hasattr(self.bus, "readfrom_mem")
+
+        ACCEL_SCALE = (2, 4, 8, 16)
+        GYRO_SCALE = (2000, 1000, 500, 250, 125)
+        ODR = (0.78, 1.5, 3.1, 6.25, 12.5, 25, 50, 100, 200, 400, 800, 1200)
+
+        # Sanity checks
+        if not self._use_i2c:
+            raise ValueError("SPI mode is not supported")
+        if gyro_odr not in ODR:
+            raise ValueError("Invalid gyro sampling rate: %d" % gyro_odr)
+        if gyro_scale not in GYRO_SCALE:
+            raise ValueError("Invalid gyro scaling: %d" % gyro_scale)
+        if accel_odr not in ODR:
+            raise ValueError("Invalid accelerometer sampling rate: %d" % accel_odr)
+        if accel_scale not in ACCEL_SCALE:
+            raise ValueError("Invalid accelerometer scaling: %d" % accel_scale)
+        if self._read_reg(_CHIP_ID) != 0x24:
+            raise OSError("No BMI270 device was found at address 0x%x" % (self.address))
+
+        # Perform initialization sequence.
+        # 0. Soft-reset
+        self._write_reg(_CMD, 0xB6)
+        time.sleep_ms(250)
+
+        # 1. Disable power save mode.
+        self._write_reg(0x7C, 0x00)
+        time.sleep_ms(10)
+
+        # 2. Prepare config load.
+        self._write_reg(0x59, 0x00)
+
+        # 3. Load config data.
+        self._write_burst(0x5E, _CONFIG_DATA)
+
+        # 4. Finish config load.
+        self._write_reg(0x59, 0x01)
+
+        # 5. Check correct initialization status.
+        if not self._poll_reg(_STATUS, 0x01):
+            raise OSError("Init sequence failed")
+
+        # 6. Configure the device in normal power mode
+        # FIFO Reset
+        self._write_reg(_CMD, 0xB0)
+        # Enable accel, gyro and temperature data.
+        self._write_reg(0x7D, 0x0E)
+        # acc_filter_perf | acc_bwp normal mode | ODR
+        self._write_reg(0x40, 0xA | (ODR.index(accel_odr) + 1))
+        # gyr_filter_perf | gyr_bwp normal mode | ODR
+        self._write_reg(0x42, 0xA | (ODR.index(gyro_odr) + 1))
+        # Disable adv_power_save | Enable fifo_self_wakeup.
+        self._write_reg(0x7C, 0x02)
+
+        # Set accelerometer scale and range.
+        self.accel_scale = 32768 / accel_scale
+        self._write_reg(0x41, ACCEL_SCALE.index(accel_scale))
+
+        # Set gyroscope scale and range.
+        self.gyro_scale = 32768 / gyro_scale
+        self._write_reg(0x43, GYRO_SCALE.index(gyro_scale))
+
+        # Allocate scratch buffer and set scale.
+        self.scratch = memoryview(array.array("h", [0, 0, 0]))
+
+    def _read_reg(self, reg, size=1):
+        buf = self.bus.readfrom_mem(self.address, reg, size)
+        if size == 1:
+            return int(buf[0])
+        return buf
+
+    def _read_reg_into(self, reg, buf):
+        self.bus.readfrom_mem_into(self.address, reg, buf)
+
+    def _write_reg(self, reg, val):
+        if isinstance(val, int):
+            val = bytes([val])
+        self.bus.writeto_mem(self.address, reg, val)
+        time.sleep_ms(1)
+
+    def _write_burst(self, reg, data, chunk=16):
+        self._write_reg(_INIT_ADDR_0, 0)
+        self._write_reg(_INIT_ADDR_1, 0)
+        for i in range(len(data) // chunk):
+            offs = i * chunk
+            self._write_reg(reg, data[offs : offs + chunk])
+            init_addr = ((i + 1) * chunk) // 2
+            self._write_reg(_INIT_ADDR_0, (init_addr & 0x0F))
+            self._write_reg(_INIT_ADDR_1, (init_addr >> 4) & 0xFF)
+
+    def _poll_reg(self, reg, mask, retry=10, delay=100):
+        for i in range(retry):
+            if self._read_reg(reg) & mask:
+                return True
+            time.sleep_ms(delay)
+        return False
+
+    def reset(self):
+        self._write_reg(_CMD, 0xB6)
+
+    def gyro(self):
+        """Returns gyroscope vector in degrees/sec."""
+        f = self.gyro_scale
+        self._read_reg_into(_DATA_14, self.scratch)
+        return (self.scratch[0] / f, self.scratch[1] / f, self.scratch[2] / f)
+
+    def accel(self):
+        """Returns acceleration vector in gravity units (9.81m/s^2)."""
+        f = self.accel_scale
+        self._read_reg_into(_DATA_8, self.scratch)
+        return (self.scratch[0] / f, self.scratch[1] / f, self.scratch[2] / f)
+
+    def magnet(self):
+        """Returns magnetometer vector."""
+        if self.bmm_magnet is not None:
+            return self.bmm_magnet.magnet()
+        return (0.0, 0.0, 0.0)
diff --git a/micropython/drivers/imu/bmi270/manifest.py b/micropython/drivers/imu/bmi270/manifest.py
new file mode 100644
index 000000000..2d89bfe9d
--- /dev/null
+++ b/micropython/drivers/imu/bmi270/manifest.py
@@ -0,0 +1,2 @@
+metadata(description="BOSCH BMI270 IMU driver.", version="1.0.0")
+module("bmi270.py", opt=3)
diff --git a/micropython/drivers/imu/bmm150/bmm150.py b/micropython/drivers/imu/bmm150/bmm150.py
new file mode 100644
index 000000000..a4845c961
--- /dev/null
+++ b/micropython/drivers/imu/bmm150/bmm150.py
@@ -0,0 +1,184 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Basic example usage:
+
+import time
+from bmm150 import BMM150
+from machine import Pin, SPI, I2C
+
+# Init in I2C mode.
+imu = BMM150(I2C(1, scl=Pin(15), sda=Pin(14)))
+
+# Or init in SPI mode.
+# TODO: Not supported yet.
+# imu = BMM150(SPI(5), cs=Pin(10))
+
+while (True):
+    print('magnetometer: x:{:>8.3f} y:{:>8.3f} z:{:>8.3f}'.format(*imu.magnet()))
+    time.sleep_ms(100)
+"""
+
+import array
+import time
+from micropython import const
+
+_DEFAULT_ADDR = const(0x10)
+_CHIP_ID = const(0x40)
+_DATA = const(0x42)
+_POWER = const(0x4B)
+_OPMODE = const(0x4C)
+_INT_STATUS = const(0x4A)
+_TRIM_X1 = const(0x5D)
+_TRIM_Y1 = const(0x5E)
+_TRIM_Z4_LSB = const(0x62)
+_TRIM_Z2_LSB = const(0x68)
+_XYAXES_FLIP = const(-4096)
+_ZHAXES_FLIP = const(-16384)
+_ODR = const((10, 2, 6, 8, 15, 20, 25, 30))
+
+
+class BMM150:
+    def __init__(
+        self,
+        bus,
+        cs=None,
+        address=_DEFAULT_ADDR,
+        magnet_odr=30,
+    ):
+        """Initalizes the Magnetometer.
+        bus: IMU bus
+        address: I2C address (in I2C mode).
+        cs: SPI CS pin (in SPI mode).
+        magnet_odr: (2, 6, 8, 10, 15, 20, 25, 30)
+        """
+        self.bus = bus
+        self.cs = cs
+        self.address = address
+        self._use_i2c = hasattr(self.bus, "readfrom_mem")
+
+        # Sanity checks
+        if not self._use_i2c:
+            raise ValueError("SPI mode is not supported")
+        if magnet_odr not in _ODR:
+            raise ValueError("Invalid sampling rate: %d" % magnet_odr)
+
+        # Perform soft reset, and power on.
+        self._write_reg(_POWER, 0x83)
+        time.sleep_ms(10)
+
+        if self._read_reg(_CHIP_ID) != 0x32:
+            raise OSError("No BMM150 device was found at address 0x%x" % (self.address))
+
+        # Configure the device.
+        # ODR | OP: Normal mode
+        self._write_reg(_OPMODE, _ODR.index(magnet_odr) << 3)
+
+        # Read trim registers.
+        trim_x1y1 = self._read_reg(_TRIM_X1, 2)
+        trim_xyz_data = self._read_reg(_TRIM_Z4_LSB, 4)
+        trim_xy1xy2 = self._read_reg(_TRIM_Z2_LSB, 10)
+
+        self.trim_x1 = trim_x1y1[0]
+        self.trim_y1 = trim_x1y1[1]
+        self.trim_x2 = trim_xyz_data[2]
+        self.trim_y2 = trim_xyz_data[3]
+        self.trim_z1 = (trim_xy1xy2[3] << 8) | trim_xy1xy2[2]
+        self.trim_z2 = (trim_xy1xy2[1] << 8) | trim_xy1xy2[0]
+        self.trim_z3 = (trim_xy1xy2[7] << 8) | trim_xy1xy2[6]
+        self.trim_z4 = (trim_xyz_data[1] << 8) | trim_xyz_data[0]
+        self.trim_xy1 = trim_xy1xy2[9]
+        self.trim_xy2 = trim_xy1xy2[8]
+        self.trim_xyz1 = ((trim_xy1xy2[5] & 0x7F) << 8) | trim_xy1xy2[4]
+
+        # Allocate scratch buffer.
+        self.scratch = memoryview(array.array("h", [0, 0, 0, 0]))
+
+    def _read_reg(self, reg, size=1):
+        buf = self.bus.readfrom_mem(self.address, reg, size)
+        if size == 1:
+            return int(buf[0])
+        return buf
+
+    def _read_reg_into(self, reg, buf):
+        self.bus.readfrom_mem_into(self.address, reg, buf)
+
+    def _write_reg(self, reg, val):
+        self.bus.writeto_mem(self.address, reg, bytes([val]))
+
+    def _compensate_x(self, raw, hall):
+        """Compensation equation ported from C driver"""
+        x = 0
+        if raw != _XYAXES_FLIP:
+            x0 = self.trim_xyz1 * 16384 / hall
+            x = x0 - 16384
+            x1 = (self.trim_xy2) * (x**2 / 268435456)
+            x2 = x1 + x * (self.trim_xy1) / 16384
+            x3 = (self.trim_x2) + 160
+            x4 = raw * ((x2 + 256) * x3)
+            x = ((x4 / 8192) + (self.trim_x1 * 8)) / 16
+        return x
+
+    def _compensate_y(self, raw, hall):
+        """Compensation equation ported from C driver"""
+        y = 0
+        if raw != _XYAXES_FLIP:
+            y0 = self.trim_xyz1 * 16384 / hall
+            y = y0 - 16384
+            y1 = self.trim_xy2 * (y**2 / 268435456)
+            y2 = y1 + y * self.trim_xy1 / 16384
+            y3 = self.trim_y2 + 160
+            y4 = raw * ((y2 + 256) * y3)
+            y = ((y4 / 8192) + (self.trim_y1 * 8)) / 16
+        return y
+
+    def _compensate_z(self, raw, hall):
+        """Compensation equation ported from C driver"""
+        z = 0
+        if raw != _ZHAXES_FLIP:
+            z0 = raw - self.trim_z4
+            z1 = hall - self.trim_xyz1
+            z2 = self.trim_z3 * z1
+            z3 = (self.trim_z1 * hall) / 32768
+            z4 = self.trim_z2 + z3
+            z5 = (z0 * 131072) - z2
+            z = (z5 / (z4 * 4)) / 16
+        return z
+
+    def magnet_raw(self):
+        for i in range(10):
+            self._read_reg_into(_DATA, self.scratch)
+            if self.scratch[3] & 0x1:
+                return (
+                    self.scratch[0] >> 3,
+                    self.scratch[1] >> 3,
+                    self.scratch[2] >> 1,
+                    self.scratch[3] >> 2,
+                )
+            time.sleep_ms(30)
+        raise OSError("Data not ready")
+
+    def magnet(self):
+        """Returns magnetometer vector."""
+        x, y, z, h = self.magnet_raw()
+        return (self._compensate_x(x, h), self._compensate_y(y, h), self._compensate_z(z, h))
diff --git a/micropython/drivers/imu/bmm150/manifest.py b/micropython/drivers/imu/bmm150/manifest.py
new file mode 100644
index 000000000..e9c7cf66b
--- /dev/null
+++ b/micropython/drivers/imu/bmm150/manifest.py
@@ -0,0 +1,2 @@
+metadata(description="BOSCH BMM150 magnetometer driver.", version="1.0.0")
+module("bmm150.py", opt=3)
diff --git a/micropython/drivers/imu/lsm6dsox/lsm6dsox.py b/micropython/drivers/imu/lsm6dsox/lsm6dsox.py
new file mode 100644
index 000000000..ca1397c66
--- /dev/null
+++ b/micropython/drivers/imu/lsm6dsox/lsm6dsox.py
@@ -0,0 +1,272 @@
+"""
+LSM6DSOX STMicro driver for MicroPython based on LSM9DS1:
+Source repo: https://github.com/hoihu/projects/tree/master/raspi-hat
+
+The MIT License (MIT)
+
+Copyright (c) 2021 Damien P. George
+Copyright (c) 2021-2023 Ibrahim Abdelkader <iabdalkader@openmv.io>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Basic example usage:
+
+import time
+from lsm6dsox import LSM6DSOX
+
+from machine import Pin, SPI, I2C
+# Init in I2C mode.
+lsm = LSM6DSOX(I2C(0, scl=Pin(13), sda=Pin(12)))
+
+# Or init in SPI mode.
+#lsm = LSM6DSOX(SPI(5), cs=Pin(10))
+
+while (True):
+    print('Accelerometer: x:{:>8.3f} y:{:>8.3f} z:{:>8.3f}'.format(*lsm.accel()))
+    print('Gyroscope:     x:{:>8.3f} y:{:>8.3f} z:{:>8.3f}'.format(*lsm.gyro()))
+    print("")
+    time.sleep_ms(100)
+"""
+
+import array
+from micropython import const
+import time
+
+_CTRL3_C = const(0x12)
+_CTRL1_XL = const(0x10)
+_CTRL8_XL = const(0x17)
+_CTRL9_XL = const(0x18)
+
+_CTRL2_G = const(0x11)
+_CTRL7_G = const(0x16)
+
+_OUTX_L_G = const(0x22)
+_OUTX_L_XL = const(0x28)
+_MLC_STATUS = const(0x38)
+
+_DEFAULT_ADDR = const(0x6A)
+_WHO_AM_I_REG = const(0x0F)
+
+_FUNC_CFG_ACCESS = const(0x01)
+_FUNC_CFG_BANK_USER = const(0)
+_FUNC_CFG_BANK_HUB = const(1)
+_FUNC_CFG_BANK_EMBED = const(2)
+
+_MLC0_SRC = const(0x70)
+_MLC_INT1 = const(0x0D)
+_TAP_CFG0 = const(0x56)
+
+_EMB_FUNC_EN_A = const(0x04)
+_EMB_FUNC_EN_B = const(0x05)
+
+
+class LSM6DSOX:
+    def __init__(
+        self,
+        bus,
+        cs=None,
+        address=_DEFAULT_ADDR,
+        gyro_odr=104,
+        accel_odr=104,
+        gyro_scale=2000,
+        accel_scale=4,
+        ucf=None,
+    ):
+        """Initalizes Gyro and Accelerator.
+        accel_odr: (0, 1.6Hz, 3.33Hz, 6.66Hz, 12.5Hz, 26Hz, 52Hz, 104Hz, 208Hz, 416Hz, 888Hz)
+        gyro_odr:  (0, 1.6Hz, 3.33Hz, 6.66Hz, 12.5Hz, 26Hz, 52Hz, 104Hz, 208Hz, 416Hz, 888Hz)
+        gyro_scale:  (245dps, 500dps, 1000dps, 2000dps)
+        accel_scale: (+/-2g, +/-4g, +/-8g, +-16g)
+        ucf: MLC program to load.
+        """
+        self.bus = bus
+        self.cs = cs
+        self.address = address
+        self._use_i2c = hasattr(self.bus, "readfrom_mem")
+
+        if not self._use_i2c and cs is None:
+            raise ValueError("A CS pin must be provided in SPI mode")
+
+        # check the id of the Accelerometer/Gyro
+        if self._read_reg(_WHO_AM_I_REG) != 108:
+            raise OSError("No LSM6DS device was found at address 0x%x" % (self.address))
+
+        # allocate scratch buffer for efficient conversions and memread op's
+        self.scratch_int = array.array("h", [0, 0, 0])
+
+        SCALE_GYRO = {250: 0, 500: 1, 1000: 2, 2000: 3}
+        SCALE_ACCEL = {2: 0, 4: 2, 8: 3, 16: 1}
+        # XL_HM_MODE = 0 by default. G_HM_MODE = 0 by default.
+        ODR = {
+            0: 0x00,
+            1.6: 0x08,
+            3.33: 0x09,
+            6.66: 0x0A,
+            12.5: 0x01,
+            26: 0x02,
+            52: 0x03,
+            104: 0x04,
+            208: 0x05,
+            416: 0x06,
+            888: 0x07,
+        }
+
+        gyro_odr = round(gyro_odr, 2)
+        accel_odr = round(accel_odr, 2)
+
+        # Sanity checks
+        if gyro_odr not in ODR:
+            raise ValueError("Invalid sampling rate: %d" % gyro_odr)
+        if gyro_scale not in SCALE_GYRO:
+            raise ValueError("invalid gyro scaling: %d" % gyro_scale)
+        if accel_odr not in ODR:
+            raise ValueError("Invalid sampling rate: %d" % accel_odr)
+        if accel_scale not in SCALE_ACCEL:
+            raise ValueError("invalid accelerometer scaling: %d" % accel_scale)
+
+        # Soft-reset the device.
+        self.reset()
+
+        # Load and configure MLC if UCF file is provided
+        if ucf is not None:
+            self.load_mlc(ucf)
+
+        # Set Gyroscope datarate and scale.
+        # Note output from LPF2 second filtering stage is selected. See Figure 18.
+        self._write_reg(_CTRL1_XL, (ODR[accel_odr] << 4) | (SCALE_ACCEL[accel_scale] << 2) | 2)
+
+        # Enable LPF2 and HPF fast-settling mode, ODR/4
+        self._write_reg(_CTRL8_XL, 0x09)
+
+        # Set Gyroscope datarate and scale.
+        self._write_reg(_CTRL2_G, (ODR[gyro_odr] << 4) | (SCALE_GYRO[gyro_scale] << 2) | 0)
+
+        self.gyro_scale = 32768 / gyro_scale
+        self.accel_scale = 32768 / accel_scale
+
+    def _read_reg(self, reg, size=1):
+        if self._use_i2c:
+            buf = self.bus.readfrom_mem(self.address, reg, size)
+        else:
+            try:
+                self.cs(0)
+                self.bus.write(bytes([reg | 0x80]))
+                buf = self.bus.read(size)
+            finally:
+                self.cs(1)
+        if size == 1:
+            return int(buf[0])
+        return [int(x) for x in buf]
+
+    def _write_reg(self, reg, val):
+        if self._use_i2c:
+            self.bus.writeto_mem(self.address, reg, bytes([val]))
+        else:
+            try:
+                self.cs(0)
+                self.bus.write(bytes([reg, val]))
+            finally:
+                self.cs(1)
+
+    def _read_reg_into(self, reg, buf):
+        if self._use_i2c:
+            self.bus.readfrom_mem_into(self.address, reg, buf)
+        else:
+            try:
+                self.cs(0)
+                self.bus.write(bytes([reg | 0x80]))
+                self.bus.readinto(buf)
+            finally:
+                self.cs(1)
+
+    def reset(self):
+        self._write_reg(_CTRL3_C, self._read_reg(_CTRL3_C) | 0x1)
+        for i in range(10):
+            if (self._read_reg(_CTRL3_C) & 0x01) == 0:
+                return
+            time.sleep_ms(10)
+        raise OSError("Failed to reset LSM6DS device.")
+
+    def set_mem_bank(self, bank):
+        cfg = self._read_reg(_FUNC_CFG_ACCESS) & 0x3F
+        self._write_reg(_FUNC_CFG_ACCESS, cfg | (bank << 6))
+
+    def set_embedded_functions(self, enable, emb_ab=None):
+        self.set_mem_bank(_FUNC_CFG_BANK_EMBED)
+        if enable:
+            self._write_reg(_EMB_FUNC_EN_A, emb_ab[0])
+            self._write_reg(_EMB_FUNC_EN_B, emb_ab[1])
+        else:
+            emb_a = self._read_reg(_EMB_FUNC_EN_A)
+            emb_b = self._read_reg(_EMB_FUNC_EN_B)
+            self._write_reg(_EMB_FUNC_EN_A, (emb_a & 0xC7))
+            self._write_reg(_EMB_FUNC_EN_B, (emb_b & 0xE6))
+            emb_ab = (emb_a, emb_b)
+
+        self.set_mem_bank(_FUNC_CFG_BANK_USER)
+        return emb_ab
+
+    def load_mlc(self, ucf):
+        # Load MLC config from file
+        with open(ucf, "r") as ucf_file:
+            for l in ucf_file:
+                if l.startswith("Ac"):
+                    v = [int(v, 16) for v in l.strip().split(" ")[1:3]]
+                    self._write_reg(v[0], v[1])
+
+        emb_ab = self.set_embedded_functions(False)
+
+        # Disable I3C interface
+        self._write_reg(_CTRL9_XL, self._read_reg(_CTRL9_XL) | 0x01)
+
+        # Enable Block Data Update
+        self._write_reg(_CTRL3_C, self._read_reg(_CTRL3_C) | 0x40)
+
+        # Route signals on interrupt pin 1
+        self.set_mem_bank(_FUNC_CFG_BANK_EMBED)
+        self._write_reg(_MLC_INT1, self._read_reg(_MLC_INT1) & 0x01)
+        self.set_mem_bank(_FUNC_CFG_BANK_USER)
+
+        # Configure interrupt pin mode
+        self._write_reg(_TAP_CFG0, self._read_reg(_TAP_CFG0) | 0x41)
+
+        self.set_embedded_functions(True, emb_ab)
+
+    def mlc_output(self):
+        buf = None
+        if self._read_reg(_MLC_STATUS) & 0x1:
+            self._read_reg(0x1A, size=12)
+            self.set_mem_bank(_FUNC_CFG_BANK_EMBED)
+            buf = self._read_reg(_MLC0_SRC, 8)
+            self.set_mem_bank(_FUNC_CFG_BANK_USER)
+        return buf
+
+    def gyro(self):
+        """Returns gyroscope vector in degrees/sec."""
+        mv = memoryview(self.scratch_int)
+        f = self.gyro_scale
+        self._read_reg_into(_OUTX_L_G, mv)
+        return (mv[0] / f, mv[1] / f, mv[2] / f)
+
+    def accel(self):
+        """Returns acceleration vector in gravity units (9.81m/s^2)."""
+        mv = memoryview(self.scratch_int)
+        f = self.accel_scale
+        self._read_reg_into(_OUTX_L_XL, mv)
+        return (mv[0] / f, mv[1] / f, mv[2] / f)
diff --git a/micropython/drivers/imu/lsm6dsox/lsm6dsox_basic.py b/micropython/drivers/imu/lsm6dsox/lsm6dsox_basic.py
new file mode 100644
index 000000000..32084a56b
--- /dev/null
+++ b/micropython/drivers/imu/lsm6dsox/lsm6dsox_basic.py
@@ -0,0 +1,15 @@
+# LSM6DSOX Basic Example.
+import time
+from lsm6dsox import LSM6DSOX
+
+from machine import Pin, I2C
+
+lsm = LSM6DSOX(I2C(0, scl=Pin(13), sda=Pin(12)))
+# Or init in SPI mode.
+# lsm = LSM6DSOX(SPI(5), cs=Pin(10))
+
+while True:
+    print("Accelerometer: x:{:>8.3f} y:{:>8.3f} z:{:>8.3f}".format(*lsm.accel()))
+    print("Gyroscope:     x:{:>8.3f} y:{:>8.3f} z:{:>8.3f}".format(*lsm.gyro()))
+    print("")
+    time.sleep_ms(100)
diff --git a/micropython/drivers/imu/lsm6dsox/lsm6dsox_mlc.py b/micropython/drivers/imu/lsm6dsox/lsm6dsox_mlc.py
new file mode 100644
index 000000000..2a53b9402
--- /dev/null
+++ b/micropython/drivers/imu/lsm6dsox/lsm6dsox_mlc.py
@@ -0,0 +1,48 @@
+# LSM6DSOX IMU MLC (Machine Learning Core) Example.
+# Download the raw UCF file, copy to storage and reset.
+
+# NOTE: The pre-trained models (UCF files) for the examples can be found here:
+# https://github.com/STMicroelectronics/STMems_Machine_Learning_Core/tree/master/application_examples/lsm6dsox
+
+import time
+from lsm6dsox import LSM6DSOX
+from machine import Pin, I2C
+
+INT_MODE = True  # Run in interrupt mode.
+INT_FLAG = False  # Set True on interrupt.
+
+
+def imu_int_handler(pin):
+    global INT_FLAG
+    INT_FLAG = True
+
+
+if INT_MODE is True:
+    int_pin = Pin(24)
+    int_pin.irq(handler=imu_int_handler, trigger=Pin.IRQ_RISING)
+
+i2c = I2C(0, scl=Pin(13), sda=Pin(12))
+
+# Vibration detection example
+UCF_FILE = "lsm6dsox_vibration_monitoring.ucf"
+UCF_LABELS = {0: "no vibration", 1: "low vibration", 2: "high vibration"}
+# NOTE: Selected data rate and scale must match the MLC data rate and scale.
+lsm = LSM6DSOX(i2c, gyro_odr=26, accel_odr=26, gyro_scale=2000, accel_scale=4, ucf=UCF_FILE)
+
+# Head gestures example
+# UCF_FILE = "lsm6dsox_head_gestures.ucf"
+# UCF_LABELS = {0:"Nod", 1:"Shake", 2:"Stationary", 3:"Swing", 4:"Walk"}
+# NOTE: Selected data rate and scale must match the MLC data rate and scale.
+# lsm = LSM6DSOX(i2c, gyro_odr=26, accel_odr=26, gyro_scale=250, accel_scale=2, ucf=UCF_FILE)
+
+print("MLC configured...")
+
+while True:
+    if INT_MODE:
+        if INT_FLAG:
+            INT_FLAG = False
+            print(UCF_LABELS[lsm.mlc_output()[0]])
+    else:
+        buf = lsm.mlc_output()
+        if buf is not None:
+            print(UCF_LABELS[buf[0]])
diff --git a/micropython/drivers/imu/lsm6dsox/manifest.py b/micropython/drivers/imu/lsm6dsox/manifest.py
new file mode 100644
index 000000000..346255fe7
--- /dev/null
+++ b/micropython/drivers/imu/lsm6dsox/manifest.py
@@ -0,0 +1,2 @@
+metadata(description="ST LSM6DSOX imu driver.", version="1.0.1")
+module("lsm6dsox.py", opt=3)
diff --git a/micropython/drivers/imu/lsm9ds1/lsm9ds1.py b/micropython/drivers/imu/lsm9ds1/lsm9ds1.py
new file mode 100644
index 000000000..e5a96ad5c
--- /dev/null
+++ b/micropython/drivers/imu/lsm9ds1/lsm9ds1.py
@@ -0,0 +1,205 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2013, 2014 Damien P. George
+Copyright (c) 2022-2023 Ibrahim Abdelkader <iabdalkader@openmv.io>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+LSM9DS1 - 9DOF inertial sensor of STMicro driver for MicroPython.
+The sensor contains an accelerometer / gyroscope / magnetometer
+Uses the internal FIFO to store up to 16 gyro/accel data, use the iter_accel_gyro generator to access it.
+
+Example usage:
+
+import time
+from lsm9ds1 import LSM9DS1
+from machine import Pin, I2C
+
+imu = LSM9DS1(I2C(1, scl=Pin(15), sda=Pin(14)))
+
+while (True):
+    #for g,a in imu.iter_accel_gyro(): print(g,a)    # using fifo
+    print('Accelerometer: x:{:>8.3f} y:{:>8.3f} z:{:>8.3f}'.format(*imu.accel()))
+    print('Magnetometer:  x:{:>8.3f} y:{:>8.3f} z:{:>8.3f}'.format(*imu.magnet()))
+    print('Gyroscope:     x:{:>8.3f} y:{:>8.3f} z:{:>8.3f}'.format(*imu.gyro()))
+    print("")
+    time.sleep_ms(100)
+"""
+
+import array
+from micropython import const
+
+
+_WHO_AM_I = const(0xF)
+_CTRL_REG1_G = const(0x10)
+_INT_GEN_SRC_G = const(0x14)
+_OUT_TEMP = const(0x15)
+_OUT_G = const(0x18)
+_CTRL_REG4_G = const(0x1E)
+_STATUS_REG = const(0x27)
+_OUT_XL = const(0x28)
+_FIFO_CTRL_REG = const(0x2E)
+_FIFO_SRC = const(0x2F)
+_OFFSET_REG_X_M = const(0x05)
+_CTRL_REG1_M = const(0x20)
+_OUT_M = const(0x28)
+_ACCEL_SCALE = const((2, 16, 4, 8))
+_GYRO_SCALE = const((245, 500, 2000))
+_MAGNET_SCALE = const((4, 8, 12, 16))
+_ODR_IMU = const((0, 14.9, 59.5, 119, 238, 476, 952))
+_ODR_MAGNET = const((0.625, 1.25, 2.5, 5, 10, 20, 40, 80))
+
+
+class LSM9DS1:
+    def __init__(
+        self,
+        bus,
+        address_imu=0x6B,
+        address_magnet=0x1E,
+        gyro_odr=952,
+        gyro_scale=245,
+        accel_odr=952,
+        accel_scale=4,
+        magnet_odr=80,
+        magnet_scale=4,
+    ):
+        """Initalizes Gyro, Accelerometer and Magnetometer.
+        bus: IMU bus
+        address_imu: IMU I2C address.
+        address_magnet: Magnetometer I2C address.
+        gyro_odr: (0, 14.9Hz, 59.5Hz, 119Hz, 238Hz, 476Hz, 952Hz)
+        gyro_scale: (245dps, 500dps, 2000dps )
+        accel_odr: (0, 14.9Hz, 59.5Hz, 119Hz, 238Hz, 476Hz, 952Hz)
+        accel_scale: (+/-2g, +/-4g, +/-8g, +-16g)
+        magnet_odr: (0.625Hz, 1.25Hz, 2.5Hz, 5Hz, 10Hz, 20Hz, 40Hz, 80Hz)
+        magnet_scale: (+/-4, +/-8, +/-12, +/-16)
+        """
+        self.bus = bus
+        self.address_imu = address_imu
+        self.address_magnet = address_magnet
+
+        # Sanity checks
+        if gyro_odr not in _ODR_IMU:
+            raise ValueError("Invalid gyro sampling rate: %d" % gyro_odr)
+        if gyro_scale not in _GYRO_SCALE:
+            raise ValueError("Invalid gyro scaling: %d" % gyro_scale)
+
+        if accel_odr not in _ODR_IMU:
+            raise ValueError("Invalid accelerometer sampling rate: %d" % accel_odr)
+        if accel_scale not in _ACCEL_SCALE:
+            raise ValueError("Invalid accelerometer scaling: %d" % accel_scale)
+
+        if magnet_odr not in _ODR_MAGNET:
+            raise ValueError("Invalid magnet sampling rate: %d" % magnet_odr)
+        if magnet_scale not in _MAGNET_SCALE:
+            raise ValueError("Invalid magnet scaling: %d" % magnet_scale)
+
+        if (self.magent_id() != b"=") or (self.gyro_id() != b"h"):
+            raise OSError(
+                "Invalid LSM9DS1 device, using address {}/{}".format(address_imu, address_magnet)
+            )
+
+        mv = memoryview(bytearray(6))
+
+        # Configure Gyroscope.
+        mv[0] = (_ODR_IMU.index(gyro_odr) << 5) | ((_GYRO_SCALE.index(gyro_scale)) << 3)
+        mv[1:4] = b"\x00\x00\x00"
+        self.bus.writeto_mem(self.address_imu, _CTRL_REG1_G, mv[:5])
+
+        # Configure Accelerometer
+        mv[0] = 0x38  # ctrl4 - enable x,y,z, outputs, no irq latching, no 4D
+        mv[1] = 0x38  # ctrl5 - enable all axes, no decimation
+        # ctrl6 - set scaling and sample rate of accel
+        mv[2] = (_ODR_IMU.index(accel_odr) << 5) | ((_ACCEL_SCALE.index(accel_scale)) << 3)
+        mv[3] = 0x00  # ctrl7 - leave at default values
+        mv[4] = 0x4  # ctrl8 - leave at default values
+        mv[5] = 0x2  # ctrl9 - FIFO enabled
+        self.bus.writeto_mem(self.address_imu, _CTRL_REG4_G, mv)
+
+        # fifo: use continous mode (overwrite old data if overflow)
+        self.bus.writeto_mem(self.address_imu, _FIFO_CTRL_REG, b"\x00")
+        self.bus.writeto_mem(self.address_imu, _FIFO_CTRL_REG, b"\xc0")
+
+        # Configure Magnetometer
+        mv[0] = 0x40 | (magnet_odr << 2)  # ctrl1: high performance mode
+        mv[1] = _MAGNET_SCALE.index(magnet_scale) << 5  # ctrl2: scale, normal mode, no reset
+        mv[2] = 0x00  # ctrl3: continous conversion, no low power, I2C
+        mv[3] = 0x08  # ctrl4: high performance z-axis
+        mv[4] = 0x00  # ctr5: no fast read, no block update
+        self.bus.writeto_mem(self.address_magnet, _CTRL_REG1_M, mv[:5])
+
+        self.gyro_scale = 32768 / gyro_scale
+        self.accel_scale = 32768 / accel_scale
+        self.scale_factor_magnet = 32768 / ((_MAGNET_SCALE.index(magnet_scale) + 1) * 4)
+
+        # Allocate scratch buffer for efficient conversions and memread op's
+        self.scratch_int = array.array("h", [0, 0, 0])
+
+    def calibrate_magnet(self, offset):
+        """
+        offset is a magnet vector that will be subtracted by the magnetometer
+        for each measurement. It is written to the magnetometer's offset register
+        """
+        import struct
+
+        offset = [int(i * self.scale_factor_magnet) for i in offset]
+        self.bus.writeto_mem(self.address_magnet, _OFFSET_REG_X_M, struct.pack("<HHH", offset))
+
+    def gyro_id(self):
+        return self.bus.readfrom_mem(self.address_imu, _WHO_AM_I, 1)
+
+    def magent_id(self):
+        return self.bus.readfrom_mem(self.address_magnet, _WHO_AM_I, 1)
+
+    def magnet(self):
+        """Returns magnetometer vector in gauss.
+        raw_values: if True, the non-scaled adc values are returned
+        """
+        mv = memoryview(self.scratch_int)
+        f = self.scale_factor_magnet
+        self.bus.readfrom_mem_into(self.address_magnet, _OUT_M | 0x80, mv)
+        return (mv[0] / f, mv[1] / f, mv[2] / f)
+
+    def gyro(self):
+        """Returns gyroscope vector in degrees/sec."""
+        mv = memoryview(self.scratch_int)
+        f = self.gyro_scale
+        self.bus.readfrom_mem_into(self.address_imu, _OUT_G | 0x80, mv)
+        return (mv[0] / f, mv[1] / f, mv[2] / f)
+
+    def accel(self):
+        """Returns acceleration vector in gravity units (9.81m/s^2)."""
+        mv = memoryview(self.scratch_int)
+        f = self.accel_scale
+        self.bus.readfrom_mem_into(self.address_imu, _OUT_XL | 0x80, mv)
+        return (mv[0] / f, mv[1] / f, mv[2] / f)
+
+    def iter_accel_gyro(self):
+        """A generator that returns tuples of (gyro,accelerometer) data from the fifo."""
+        while True:
+            fifo_state = int.from_bytes(
+                self.bus.readfrom_mem(self.address_imu, _FIFO_SRC, 1), "big"
+            )
+            if fifo_state & 0x3F:
+                # print("Available samples=%d" % (fifo_state & 0x1f))
+                yield self.gyro(), self.accel()
+            else:
+                break
diff --git a/micropython/drivers/imu/lsm9ds1/manifest.py b/micropython/drivers/imu/lsm9ds1/manifest.py
new file mode 100644
index 000000000..c15c914b3
--- /dev/null
+++ b/micropython/drivers/imu/lsm9ds1/manifest.py
@@ -0,0 +1,2 @@
+metadata(description="Driver for ST LSM9DS1 IMU.", version="1.0.0")
+module("lsm9ds1.py", opt=3)
diff --git a/micropython/drivers/led/neopixel/manifest.py b/micropython/drivers/led/neopixel/manifest.py
new file mode 100644
index 000000000..02a319002
--- /dev/null
+++ b/micropython/drivers/led/neopixel/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="WS2812/NeoPixel driver.", version="0.1.0")
+
+module("neopixel.py", opt=3)
diff --git a/micropython/drivers/led/neopixel/neopixel.py b/micropython/drivers/led/neopixel/neopixel.py
new file mode 100644
index 000000000..caa12dc84
--- /dev/null
+++ b/micropython/drivers/led/neopixel/neopixel.py
@@ -0,0 +1,50 @@
+# NeoPixel driver for MicroPython
+# MIT license; Copyright (c) 2016 Damien P. George, 2021 Jim Mussared
+
+from machine import bitstream
+
+
+class NeoPixel:
+    # G R B W
+    ORDER = (1, 0, 2, 3)
+
+    def __init__(self, pin, n, bpp=3, timing=1):
+        self.pin = pin
+        self.n = n
+        self.bpp = bpp
+        self.buf = bytearray(n * bpp)
+        self.pin.init(pin.OUT)
+        # Timing arg can either be 1 for 800kHz or 0 for 400kHz,
+        # or a user-specified timing ns tuple (high_0, low_0, high_1, low_1).
+        self.timing = (
+            ((400, 850, 800, 450) if timing else (800, 1700, 1600, 900))
+            if isinstance(timing, int)
+            else timing
+        )
+
+    def __len__(self):
+        return self.n
+
+    def __setitem__(self, i, v):
+        offset = i * self.bpp
+        for i in range(self.bpp):
+            self.buf[offset + self.ORDER[i]] = v[i]
+
+    def __getitem__(self, i):
+        offset = i * self.bpp
+        return tuple(self.buf[offset + self.ORDER[i]] for i in range(self.bpp))
+
+    def fill(self, v):
+        b = self.buf
+        l = len(self.buf)
+        bpp = self.bpp
+        for i in range(bpp):
+            c = v[i]
+            j = self.ORDER[i]
+            while j < l:
+                b[j] = c
+                j += bpp
+
+    def write(self):
+        # BITSTREAM_TYPE_HIGH_LOW = 0
+        bitstream(self.pin, 0, self.timing, self.buf)
diff --git a/micropython/drivers/radio/nrf24l01/manifest.py b/micropython/drivers/radio/nrf24l01/manifest.py
new file mode 100644
index 000000000..24276ee4b
--- /dev/null
+++ b/micropython/drivers/radio/nrf24l01/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="nrf24l01 2.4GHz radio driver.", version="0.2.0")
+
+module("nrf24l01.py", opt=3)
diff --git a/micropython/drivers/radio/nrf24l01/nrf24l01.py b/micropython/drivers/radio/nrf24l01/nrf24l01.py
new file mode 100644
index 000000000..9fbadcd60
--- /dev/null
+++ b/micropython/drivers/radio/nrf24l01/nrf24l01.py
@@ -0,0 +1,266 @@
+"""NRF24L01 driver for MicroPython"""
+
+from micropython import const
+import utime
+
+# nRF24L01+ registers
+CONFIG = const(0x00)
+EN_RXADDR = const(0x02)
+SETUP_AW = const(0x03)
+SETUP_RETR = const(0x04)
+RF_CH = const(0x05)
+RF_SETUP = const(0x06)
+STATUS = const(0x07)
+RX_ADDR_P0 = const(0x0A)
+TX_ADDR = const(0x10)
+RX_PW_P0 = const(0x11)
+FIFO_STATUS = const(0x17)
+DYNPD = const(0x1C)
+
+# CONFIG register
+EN_CRC = const(0x08)  # enable CRC
+CRCO = const(0x04)  # CRC encoding scheme; 0=1 byte, 1=2 bytes
+PWR_UP = const(0x02)  # 1=power up, 0=power down
+PRIM_RX = const(0x01)  # RX/TX control; 0=PTX, 1=PRX
+
+# RF_SETUP register
+POWER_0 = const(0x00)  # -18 dBm
+POWER_1 = const(0x02)  # -12 dBm
+POWER_2 = const(0x04)  # -6 dBm
+POWER_3 = const(0x06)  # 0 dBm
+SPEED_1M = const(0x00)
+SPEED_2M = const(0x08)
+SPEED_250K = const(0x20)
+
+# STATUS register
+RX_DR = const(0x40)  # RX data ready; write 1 to clear
+TX_DS = const(0x20)  # TX data sent; write 1 to clear
+MAX_RT = const(0x10)  # max retransmits reached; write 1 to clear
+
+# FIFO_STATUS register
+RX_EMPTY = const(0x01)  # 1 if RX FIFO is empty
+
+# constants for instructions
+R_RX_PL_WID = const(0x60)  # read RX payload width
+R_RX_PAYLOAD = const(0x61)  # read RX payload
+W_TX_PAYLOAD = const(0xA0)  # write TX payload
+FLUSH_TX = const(0xE1)  # flush TX FIFO
+FLUSH_RX = const(0xE2)  # flush RX FIFO
+NOP = const(0xFF)  # use to read STATUS register
+
+
+class NRF24L01:
+    def __init__(self, spi, cs, ce, channel=46, payload_size=16):
+        assert payload_size <= 32
+
+        self.buf = bytearray(1)
+
+        # store the pins
+        self.spi = spi
+        self.cs = cs
+        self.ce = ce
+
+        # init the SPI bus and pins
+        self.init_spi(4000000)
+
+        # reset everything
+        ce.init(ce.OUT, value=0)
+        cs.init(cs.OUT, value=1)
+
+        self.payload_size = payload_size
+        self.pipe0_read_addr = None
+        utime.sleep_ms(5)
+
+        # set address width to 5 bytes and check for device present
+        self.reg_write(SETUP_AW, 0b11)
+        if self.reg_read(SETUP_AW) != 0b11:
+            raise OSError("nRF24L01+ Hardware not responding")
+
+        # disable dynamic payloads
+        self.reg_write(DYNPD, 0)
+
+        # auto retransmit delay: 1750us
+        # auto retransmit count: 8
+        self.reg_write(SETUP_RETR, (6 << 4) | 8)
+
+        # set rf power and speed
+        self.set_power_speed(POWER_3, SPEED_250K)  # Best for point to point links
+
+        # init CRC
+        self.set_crc(2)
+
+        # clear status flags
+        self.reg_write(STATUS, RX_DR | TX_DS | MAX_RT)
+
+        # set channel
+        self.set_channel(channel)
+
+        # flush buffers
+        self.flush_rx()
+        self.flush_tx()
+
+    def init_spi(self, baudrate):
+        try:
+            master = self.spi.MASTER
+        except AttributeError:
+            self.spi.init(baudrate=baudrate, polarity=0, phase=0)
+        else:
+            self.spi.init(master, baudrate=baudrate, polarity=0, phase=0)
+
+    def reg_read(self, reg):
+        self.cs(0)
+        self.spi.readinto(self.buf, reg)
+        self.spi.readinto(self.buf)
+        self.cs(1)
+        return self.buf[0]
+
+    def reg_write_bytes(self, reg, buf):
+        self.cs(0)
+        self.spi.readinto(self.buf, 0x20 | reg)
+        self.spi.write(buf)
+        self.cs(1)
+        return self.buf[0]
+
+    def reg_write(self, reg, value):
+        self.cs(0)
+        self.spi.readinto(self.buf, 0x20 | reg)
+        ret = self.buf[0]
+        self.spi.readinto(self.buf, value)
+        self.cs(1)
+        return ret
+
+    def read_status(self):
+        self.cs(0)
+        # STATUS register is always shifted during command transmit
+        self.spi.readinto(self.buf, NOP)
+        self.cs(1)
+        return self.buf[0]
+
+    def flush_rx(self):
+        self.cs(0)
+        self.spi.readinto(self.buf, FLUSH_RX)
+        self.cs(1)
+
+    def flush_tx(self):
+        self.cs(0)
+        self.spi.readinto(self.buf, FLUSH_TX)
+        self.cs(1)
+
+    # power is one of POWER_x defines; speed is one of SPEED_x defines
+    def set_power_speed(self, power, speed):
+        setup = self.reg_read(RF_SETUP) & 0b11010001
+        self.reg_write(RF_SETUP, setup | power | speed)
+
+    # length in bytes: 0, 1 or 2
+    def set_crc(self, length):
+        config = self.reg_read(CONFIG) & ~(CRCO | EN_CRC)
+        if length == 0:
+            pass
+        elif length == 1:
+            config |= EN_CRC
+        else:
+            config |= EN_CRC | CRCO
+        self.reg_write(CONFIG, config)
+
+    def set_channel(self, channel):
+        self.reg_write(RF_CH, min(channel, 125))
+
+    # address should be a bytes object 5 bytes long
+    def open_tx_pipe(self, address):
+        assert len(address) == 5
+        self.reg_write_bytes(RX_ADDR_P0, address)
+        self.reg_write_bytes(TX_ADDR, address)
+        self.reg_write(RX_PW_P0, self.payload_size)
+
+    # address should be a bytes object 5 bytes long
+    # pipe 0 and 1 have 5 byte address
+    # pipes 2-5 use same 4 most-significant bytes as pipe 1, plus 1 extra byte
+    def open_rx_pipe(self, pipe_id, address):
+        assert len(address) == 5
+        assert 0 <= pipe_id <= 5
+        if pipe_id == 0:
+            self.pipe0_read_addr = address
+        if pipe_id < 2:
+            self.reg_write_bytes(RX_ADDR_P0 + pipe_id, address)
+        else:
+            self.reg_write(RX_ADDR_P0 + pipe_id, address[0])
+        self.reg_write(RX_PW_P0 + pipe_id, self.payload_size)
+        self.reg_write(EN_RXADDR, self.reg_read(EN_RXADDR) | (1 << pipe_id))
+
+    def start_listening(self):
+        self.reg_write(CONFIG, self.reg_read(CONFIG) | PWR_UP | PRIM_RX)
+        self.reg_write(STATUS, RX_DR | TX_DS | MAX_RT)
+
+        if self.pipe0_read_addr is not None:
+            self.reg_write_bytes(RX_ADDR_P0, self.pipe0_read_addr)
+
+        self.flush_rx()
+        self.flush_tx()
+        self.ce(1)
+        utime.sleep_us(130)
+
+    def stop_listening(self):
+        self.ce(0)
+        self.flush_tx()
+        self.flush_rx()
+
+    # returns True if any data available to recv
+    def any(self):
+        return not bool(self.reg_read(FIFO_STATUS) & RX_EMPTY)
+
+    def recv(self):
+        # get the data
+        self.cs(0)
+        self.spi.readinto(self.buf, R_RX_PAYLOAD)
+        buf = self.spi.read(self.payload_size)
+        self.cs(1)
+        # clear RX ready flag
+        self.reg_write(STATUS, RX_DR)
+
+        return buf
+
+    # blocking wait for tx complete
+    def send(self, buf, timeout=500):
+        self.send_start(buf)
+        start = utime.ticks_ms()
+        result = None
+        while result is None and utime.ticks_diff(utime.ticks_ms(), start) < timeout:
+            result = self.send_done()  # 1 == success, 2 == fail
+
+        if result is None:
+            # timed out, cancel sending and power down the module
+            self.flush_tx()
+            self.reg_write(CONFIG, self.reg_read(CONFIG) & ~PWR_UP)
+            raise OSError("timed out")
+
+        if result == 2:
+            raise OSError("send failed")
+
+    # non-blocking tx
+    def send_start(self, buf):
+        # power up
+        self.reg_write(CONFIG, (self.reg_read(CONFIG) | PWR_UP) & ~PRIM_RX)
+        utime.sleep_us(1500)  # needs to be 1.5ms
+        # send the data
+        self.cs(0)
+        self.spi.readinto(self.buf, W_TX_PAYLOAD)
+        self.spi.write(buf)
+        if len(buf) < self.payload_size:
+            self.spi.write(b"\x00" * (self.payload_size - len(buf)))  # pad out data
+        self.cs(1)
+
+        # enable the chip so it can send the data
+        self.ce(1)
+        utime.sleep_us(15)  # needs to be >10us
+        self.ce(0)
+
+    # returns None if send still in progress, 1 for success, 2 for fail
+    def send_done(self):
+        status = self.read_status()
+        if not (status & (TX_DS | MAX_RT)):
+            return None  # tx not finished
+
+        # either finished or failed: get and clear status flags, power down
+        status = self.reg_write(STATUS, RX_DR | TX_DS | MAX_RT)
+        self.reg_write(CONFIG, self.reg_read(CONFIG) & ~PWR_UP)
+        return 1 if status & TX_DS else 2
diff --git a/micropython/drivers/radio/nrf24l01/nrf24l01test.py b/micropython/drivers/radio/nrf24l01/nrf24l01test.py
new file mode 100644
index 000000000..a0c4b76f4
--- /dev/null
+++ b/micropython/drivers/radio/nrf24l01/nrf24l01test.py
@@ -0,0 +1,148 @@
+"""Test for nrf24l01 module.  Portable between MicroPython targets."""
+
+import sys
+import struct
+import utime
+from machine import Pin, SPI, SoftSPI
+from nrf24l01 import NRF24L01
+from micropython import const
+
+# Responder pause between receiving data and checking for further packets.
+_RX_POLL_DELAY = const(15)
+# Responder pauses an additional _RESPONER_SEND_DELAY ms after receiving data and before
+# transmitting to allow the (remote) initiator time to get into receive mode. The
+# initiator may be a slow device. Value tested with Pyboard, ESP32 and ESP8266.
+_RESPONDER_SEND_DELAY = const(10)
+
+if sys.platform == "pyboard":
+    spi = SPI(2)  # miso : Y7, mosi : Y8, sck : Y6
+    cfg = {"spi": spi, "csn": "Y5", "ce": "Y4"}
+elif sys.platform == "esp8266":  # Hardware SPI
+    spi = SPI(1)  # miso : 12, mosi : 13, sck : 14
+    cfg = {"spi": spi, "csn": 4, "ce": 5}
+elif sys.platform == "esp32":  # Software SPI
+    spi = SoftSPI(sck=Pin(25), mosi=Pin(33), miso=Pin(32))
+    cfg = {"spi": spi, "csn": 26, "ce": 27}
+elif sys.platform == "rp2":  # Hardware SPI with explicit pin definitions
+    spi = SPI(0, sck=Pin(2), mosi=Pin(3), miso=Pin(4))
+    cfg = {"spi": spi, "csn": 5, "ce": 6}
+else:
+    raise ValueError("Unsupported platform {}".format(sys.platform))
+
+# Addresses are in little-endian format. They correspond to big-endian
+# 0xf0f0f0f0e1, 0xf0f0f0f0d2
+pipes = (b"\xe1\xf0\xf0\xf0\xf0", b"\xd2\xf0\xf0\xf0\xf0")
+
+
+def initiator():
+    csn = Pin(cfg["csn"], mode=Pin.OUT, value=1)
+    ce = Pin(cfg["ce"], mode=Pin.OUT, value=0)
+    spi = cfg["spi"]
+    nrf = NRF24L01(spi, csn, ce, payload_size=8)
+
+    nrf.open_tx_pipe(pipes[0])
+    nrf.open_rx_pipe(1, pipes[1])
+    nrf.start_listening()
+
+    num_needed = 16
+    num_successes = 0
+    num_failures = 0
+    led_state = 0
+
+    print("NRF24L01 initiator mode, sending %d packets..." % num_needed)
+
+    while num_successes < num_needed and num_failures < num_needed:
+        # stop listening and send packet
+        nrf.stop_listening()
+        millis = utime.ticks_ms()
+        led_state = max(1, (led_state << 1) & 0x0F)
+        print("sending:", millis, led_state)
+        try:
+            nrf.send(struct.pack("ii", millis, led_state))
+        except OSError:
+            pass
+
+        # start listening again
+        nrf.start_listening()
+
+        # wait for response, with 250ms timeout
+        start_time = utime.ticks_ms()
+        timeout = False
+        while not nrf.any() and not timeout:
+            if utime.ticks_diff(utime.ticks_ms(), start_time) > 250:
+                timeout = True
+
+        if timeout:
+            print("failed, response timed out")
+            num_failures += 1
+
+        else:
+            # recv packet
+            (got_millis,) = struct.unpack("i", nrf.recv())
+
+            # print response and round-trip delay
+            print(
+                "got response:",
+                got_millis,
+                "(delay",
+                utime.ticks_diff(utime.ticks_ms(), got_millis),
+                "ms)",
+            )
+            num_successes += 1
+
+        # delay then loop
+        utime.sleep_ms(250)
+
+    print("initiator finished sending; successes=%d, failures=%d" % (num_successes, num_failures))
+
+
+def responder():
+    csn = Pin(cfg["csn"], mode=Pin.OUT, value=1)
+    ce = Pin(cfg["ce"], mode=Pin.OUT, value=0)
+    spi = cfg["spi"]
+    nrf = NRF24L01(spi, csn, ce, payload_size=8)
+
+    nrf.open_tx_pipe(pipes[1])
+    nrf.open_rx_pipe(1, pipes[0])
+    nrf.start_listening()
+
+    print("NRF24L01 responder mode, waiting for packets... (ctrl-C to stop)")
+
+    while True:
+        if nrf.any():
+            while nrf.any():
+                buf = nrf.recv()
+                millis, led_state = struct.unpack("ii", buf)
+                print("received:", millis, led_state)
+                for led in leds:
+                    if led_state & 1:
+                        led.on()
+                    else:
+                        led.off()
+                    led_state >>= 1
+                utime.sleep_ms(_RX_POLL_DELAY)
+
+            # Give initiator time to get into receive mode.
+            utime.sleep_ms(_RESPONDER_SEND_DELAY)
+            nrf.stop_listening()
+            try:
+                nrf.send(struct.pack("i", millis))
+            except OSError:
+                pass
+            print("sent response")
+            nrf.start_listening()
+
+
+try:
+    import pyb
+
+    leds = [pyb.LED(i + 1) for i in range(4)]
+except:
+    leds = []
+
+print("NRF24L01 test module loaded")
+print("NRF24L01 pinout for test:")
+print("    CE on", cfg["ce"])
+print("    CSN on", cfg["csn"])
+print("    SPI on", cfg["spi"])
+print("run nrf24l01test.responder() on responder, then nrf24l01test.initiator() on initiator")
diff --git a/micropython/drivers/sensor/dht/dht.py b/micropython/drivers/sensor/dht/dht.py
new file mode 100644
index 000000000..4624ae2ad
--- /dev/null
+++ b/micropython/drivers/sensor/dht/dht.py
@@ -0,0 +1,47 @@
+# DHT11/DHT22 driver for MicroPython on ESP8266
+# MIT license; Copyright (c) 2016 Damien P. George
+
+import sys
+import machine
+
+if hasattr(machine, "dht_readinto"):
+    from machine import dht_readinto
+elif sys.platform.startswith("esp"):
+    from esp import dht_readinto
+elif sys.platform == "pyboard":
+    from pyb import dht_readinto
+else:
+    dht_readinto = __import__(sys.platform).dht_readinto
+
+del machine
+
+
+class DHTBase:
+    def __init__(self, pin):
+        self.pin = pin
+        self.buf = bytearray(5)
+
+    def measure(self):
+        buf = self.buf
+        dht_readinto(self.pin, buf)
+        if (buf[0] + buf[1] + buf[2] + buf[3]) & 0xFF != buf[4]:
+            raise Exception("checksum error")
+
+
+class DHT11(DHTBase):
+    def humidity(self):
+        return self.buf[0]
+
+    def temperature(self):
+        return self.buf[2]
+
+
+class DHT22(DHTBase):
+    def humidity(self):
+        return (self.buf[0] << 8 | self.buf[1]) * 0.1
+
+    def temperature(self):
+        t = ((self.buf[2] & 0x7F) << 8 | self.buf[3]) * 0.1
+        if self.buf[2] & 0x80:
+            t = -t
+        return t
diff --git a/micropython/drivers/sensor/dht/manifest.py b/micropython/drivers/sensor/dht/manifest.py
new file mode 100644
index 000000000..964e8e252
--- /dev/null
+++ b/micropython/drivers/sensor/dht/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="DHT11 & DHT22 temperature/humidity sensor driver.", version="0.1.0")
+
+module("dht.py", opt=3)
diff --git a/micropython/drivers/sensor/ds18x20/ds18x20.py b/micropython/drivers/sensor/ds18x20/ds18x20.py
new file mode 100644
index 000000000..ad2d9f52c
--- /dev/null
+++ b/micropython/drivers/sensor/ds18x20/ds18x20.py
@@ -0,0 +1,52 @@
+# DS18x20 temperature sensor driver for MicroPython.
+# MIT license; Copyright (c) 2016 Damien P. George
+
+from micropython import const
+
+_CONVERT = const(0x44)
+_RD_SCRATCH = const(0xBE)
+_WR_SCRATCH = const(0x4E)
+
+
+class DS18X20:
+    def __init__(self, onewire):
+        self.ow = onewire
+        self.buf = bytearray(9)
+
+    def scan(self):
+        return [rom for rom in self.ow.scan() if rom[0] in (0x10, 0x22, 0x28)]
+
+    def convert_temp(self):
+        self.ow.reset(True)
+        self.ow.writebyte(self.ow.SKIP_ROM)
+        self.ow.writebyte(_CONVERT)
+
+    def read_scratch(self, rom):
+        self.ow.reset(True)
+        self.ow.select_rom(rom)
+        self.ow.writebyte(_RD_SCRATCH)
+        self.ow.readinto(self.buf)
+        if self.ow.crc8(self.buf):
+            raise Exception("CRC error")
+        return self.buf
+
+    def write_scratch(self, rom, buf):
+        self.ow.reset(True)
+        self.ow.select_rom(rom)
+        self.ow.writebyte(_WR_SCRATCH)
+        self.ow.write(buf)
+
+    def read_temp(self, rom):
+        buf = self.read_scratch(rom)
+        if rom[0] == 0x10:
+            if buf[1]:
+                t = buf[0] >> 1 | 0x80
+                t = -((~t + 1) & 0xFF)
+            else:
+                t = buf[0] >> 1
+            return t - 0.25 + (buf[7] - buf[6]) / buf[7]
+        else:
+            t = buf[1] << 8 | buf[0]
+            if t & 0x8000:  # sign bit set
+                t = -((t ^ 0xFFFF) + 1)
+            return t / 16
diff --git a/micropython/drivers/sensor/ds18x20/manifest.py b/micropython/drivers/sensor/ds18x20/manifest.py
new file mode 100644
index 000000000..6ced882f7
--- /dev/null
+++ b/micropython/drivers/sensor/ds18x20/manifest.py
@@ -0,0 +1,4 @@
+metadata(description="DS18x20 temperature sensor driver.", version="0.1.0")
+
+require("onewire")
+module("ds18x20.py", opt=3)
diff --git a/micropython/drivers/sensor/hs3003/hs3003.py b/micropython/drivers/sensor/hs3003/hs3003.py
new file mode 100644
index 000000000..003501649
--- /dev/null
+++ b/micropython/drivers/sensor/hs3003/hs3003.py
@@ -0,0 +1,64 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+HS3003 driver for MicroPython.
+
+Example usage:
+
+import time
+from hs3003 import HS3003
+from machine import Pin, I2C
+
+bus = I2C(1, scl=Pin(15), sda=Pin(14))
+hts = HS3003(bus)
+
+while True:
+    rH   = hts.humidity()
+    temp = hts.temperature()
+    print ("rH: %.2f%% T: %.2fC" %(rH, temp))
+    time.sleep_ms(100)
+"""
+
+import struct
+
+
+class HS3003:
+    def __init__(self, bus, address=0x44):
+        self.bus = bus
+        self.address = address
+
+    def _read_data(self):
+        # Init measurement mode
+        self.bus.writeto(self.address, b"")
+        # Data fetch
+        return struct.unpack(">HH", self.bus.readfrom(self.address, 4))
+
+    def humidity(self):
+        """Returns the relative humidity in percent."""
+        h, t = self._read_data()
+        return ((h & 0x3FFF) / 16383) * 100
+
+    def temperature(self):
+        """Returns the temperature in degrees Celsius."""
+        h, t = self._read_data()
+        return ((t >> 2) / 16383) * 165 - 40
diff --git a/micropython/drivers/sensor/hs3003/manifest.py b/micropython/drivers/sensor/hs3003/manifest.py
new file mode 100644
index 000000000..8409c76d8
--- /dev/null
+++ b/micropython/drivers/sensor/hs3003/manifest.py
@@ -0,0 +1,2 @@
+metadata(description="Renesas HS3003 Humidity and Temperature sensor driver.", version="1.0.0")
+module("hs3003.py", opt=3)
diff --git a/micropython/drivers/sensor/hts221/hts221.py b/micropython/drivers/sensor/hts221/hts221.py
new file mode 100644
index 000000000..c6cd51f48
--- /dev/null
+++ b/micropython/drivers/sensor/hts221/hts221.py
@@ -0,0 +1,91 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2013-2022 Ibrahim Abdelkader <iabdalkader@openmv.io>
+Copyright (c) 2013-2022 Kwabena W. Agyeman <kwagyeman@openmv.io>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+HTS221 driver driver for MicroPython.
+Original source: https://github.com/ControlEverythingCommunity/HTS221/blob/master/Python/HTS221.py
+
+Example usage:
+
+import time
+import hts221
+from machine import Pin, I2C
+
+bus = I2C(1, scl=Pin(15), sda=Pin(14))
+hts = hts221.HTS221(bus)
+
+while (True):
+    rH   = hts.humidity()
+    temp = hts.temperature()
+    print ("rH: %.2f%% T: %.2fC" %(rH, temp))
+    time.sleep_ms(100)
+"""
+
+import struct
+
+
+class HTS221:
+    def __init__(self, i2c, data_rate=1, address=0x5F):
+        self.bus = i2c
+        self.odr = data_rate
+        self.slv_addr = address
+
+        # Set configuration register
+        # Humidity and temperature average configuration
+        self.bus.writeto_mem(self.slv_addr, 0x10, b"\x1b")
+
+        # Set control register
+        # PD | BDU | ODR
+        cfg = 0x80 | 0x04 | (self.odr & 0x3)
+        self.bus.writeto_mem(self.slv_addr, 0x20, bytes([cfg]))
+
+        # Read Calibration values from non-volatile memory of the device
+        # Humidity Calibration values
+        self.H0 = self._read_reg(0x30, 1) / 2
+        self.H1 = self._read_reg(0x31, 1) / 2
+        self.H2 = self._read_reg(0x36, 2)
+        self.H3 = self._read_reg(0x3A, 2)
+
+        # Temperature Calibration values
+        raw = self._read_reg(0x35, 1)
+        self.T0 = ((raw & 0x03) * 256) + self._read_reg(0x32, 1)
+        self.T1 = ((raw & 0x0C) * 64) + self._read_reg(0x33, 1)
+        self.T2 = self._read_reg(0x3C, 2)
+        self.T3 = self._read_reg(0x3E, 2)
+
+    def _read_reg(self, reg_addr, size):
+        fmt = "B" if size == 1 else "H"
+        reg_addr = reg_addr if size == 1 else reg_addr | 0x80
+        return struct.unpack(fmt, self.bus.readfrom_mem(self.slv_addr, reg_addr, size))[0]
+
+    def humidity(self):
+        rH = self._read_reg(0x28, 2)
+        return (self.H1 - self.H0) * (rH - self.H2) / (self.H3 - self.H2) + self.H0
+
+    def temperature(self):
+        temp = self._read_reg(0x2A, 2)
+        if temp > 32767:
+            temp -= 65536
+        return ((self.T1 - self.T0) / 8.0) * (temp - self.T2) / (self.T3 - self.T2) + (
+            self.T0 / 8.0
+        )
diff --git a/micropython/drivers/sensor/hts221/manifest.py b/micropython/drivers/sensor/hts221/manifest.py
new file mode 100644
index 000000000..d85edaac8
--- /dev/null
+++ b/micropython/drivers/sensor/hts221/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="HTS221 temperature/humidity sensor driver.", version="0.1.0")
+
+module("hts221.py", opt=3)
diff --git a/micropython/drivers/sensor/lps22h/lps22h.py b/micropython/drivers/sensor/lps22h/lps22h.py
new file mode 100644
index 000000000..7dec72528
--- /dev/null
+++ b/micropython/drivers/sensor/lps22h/lps22h.py
@@ -0,0 +1,111 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2016-2019 shaoziyang <shaoziyang@micropython.org.cn>
+Copyright (c) 2022 Ibrahim Abdelkader <iabdalkader@openmv.io>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+LPS22HB/HH pressure sensor driver for MicroPython.
+
+Example usage:
+
+import time
+from lps22h import LPS22H
+from machine import Pin, I2C
+
+bus = I2C(1, scl=Pin(15), sda=Pin(14))
+lps = LPS22H(bus, oneshot=False)
+
+while (True):
+    print("Pressure: %.2f hPa Temperature: %.2f C"%(lps.pressure(), lps.temperature()))
+    time.sleep_ms(10)
+"""
+
+import machine
+from micropython import const
+
+_LPS22_CTRL_REG1 = const(0x10)
+_LPS22_CTRL_REG2 = const(0x11)
+_LPS22_STATUS = const(0x27)
+_LPS22_TEMP_OUT_L = const(0x2B)
+_LPS22_PRESS_OUT_XL = const(0x28)
+_LPS22_PRESS_OUT_L = const(0x29)
+
+
+class LPS22H:
+    def __init__(self, i2c, address=0x5C, oneshot=False):
+        self.i2c = i2c
+        self.addr = address
+        self.oneshot = oneshot
+        self.buf = bytearray(1)
+        # ODR=1 EN_LPFP=1 BDU=1
+        self._write_reg(_LPS22_CTRL_REG1, 0x1A)
+        self.set_oneshot_mode(self.oneshot)
+
+    def _int16(self, d):
+        return d if d < 0x8000 else d - 0x10000
+
+    def _write_reg(self, reg, dat):
+        self.buf[0] = dat
+        self.i2c.writeto_mem(self.addr, reg, self.buf)
+
+    def _read_reg(self, reg, width=8):
+        self.i2c.readfrom_mem_into(self.addr, reg, self.buf)
+        val = self.buf[0]
+        if width == 16:
+            val |= self._read_reg(reg + 1) << 8
+        return val
+
+    def _tigger_oneshot(self, b):
+        if self.oneshot:
+            self._write_reg(_LPS22_CTRL_REG2, self._read_reg(_LPS22_CTRL_REG2) | 0x01)
+            self._read_reg(0x28 + b * 2)
+            while True:
+                if self._read_reg(_LPS22_STATUS) & b:
+                    return
+                machine.idle()
+
+    def set_oneshot_mode(self, oneshot):
+        self._read_reg(_LPS22_CTRL_REG1)
+        self.oneshot = oneshot
+        if oneshot:
+            self.buf[0] &= 0x0F
+        else:
+            self.buf[0] |= 0x10
+        self._write_reg(_LPS22_CTRL_REG1, self.buf[0])
+
+    def pressure(self):
+        if self.oneshot:
+            self._tigger_oneshot(1)
+        return (
+            self._read_reg(_LPS22_PRESS_OUT_XL) + self._read_reg(_LPS22_PRESS_OUT_L, 16) * 256
+        ) / 4096
+
+    def temperature(self):
+        if self.oneshot:
+            self._tigger_oneshot(2)
+        return self._int16(self._read_reg(_LPS22_TEMP_OUT_L, 16)) / 100
+
+    def altitude(self):
+        return (
+            (((1013.25 / self.pressure()) ** (1 / 5.257)) - 1.0)
+            * (self.temperature() + 273.15)
+            / 0.0065
+        )
diff --git a/micropython/drivers/sensor/lps22h/manifest.py b/micropython/drivers/sensor/lps22h/manifest.py
new file mode 100644
index 000000000..971cbfdcb
--- /dev/null
+++ b/micropython/drivers/sensor/lps22h/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="LPS22H temperature/pressure sensor driver.", version="0.1.0")
+
+module("lps22h.py", opt=3)
diff --git a/micropython/drivers/sensor/mhz19/manifest.py b/micropython/drivers/sensor/mhz19/manifest.py
new file mode 100644
index 000000000..0647201bd
--- /dev/null
+++ b/micropython/drivers/sensor/mhz19/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="Driver for MH-Z19 CO2 sensor.", version="0.1.0")
+
+module("mhz19.py", opt=3)
diff --git a/micropython/drivers/sensor/mhz19/mhz19.py b/micropython/drivers/sensor/mhz19/mhz19.py
new file mode 100644
index 000000000..40eff7ed2
--- /dev/null
+++ b/micropython/drivers/sensor/mhz19/mhz19.py
@@ -0,0 +1,48 @@
+# MH-Z19 CO2 sensor driver for MicroPython.
+# MIT license; Copyright (c) 2018 Fredrik Strupe
+
+import machine
+import utime
+
+
+class TimeoutError(Exception):
+    pass
+
+
+class MHZ19:
+    """MH-Z19 CO2 sensor driver"""
+
+    def __init__(self, pin, max_value=5000):
+        """
+        Args:
+            pin: the pin that the PWM pin on the MH-Z19 is connected to.
+            max_value: upper bound of measuring range. usually 2000 or 5000.
+        """
+        self.pin = pin
+        self.max_value = max_value
+
+    def _wait_on_condition(self, cond, timeout=5000):
+        start = utime.ticks_ms()
+        while not cond():
+            if utime.ticks_diff(utime.ticks_ms(), start) > timeout:
+                raise TimeoutError
+
+    def pwm_read(self):
+        """Read CO2 value via PWM pin.
+
+        Reading usually takes 1-2 seconds.
+
+        Returns:
+            CO2 value in ppm (parts per million), with an accuracy of
+            ±(50 + result * 0.05) ppm.
+        Raises:
+            TimeoutError: if the reading takes more than 5 seconds.
+        """
+        # Wait until a new cycle starts
+        self._wait_on_condition(lambda: self.pin.value() == 0)
+
+        # Measure high and low duration during cycle
+        t_h = machine.time_pulse_us(self.pin, 1, 1500000)
+        t_l = machine.time_pulse_us(self.pin, 0, 1500000)
+
+        return self.max_value * (t_h - 2000) / (t_h + t_l - 4000)
diff --git a/micropython/drivers/storage/sdcard/manifest.py b/micropython/drivers/storage/sdcard/manifest.py
new file mode 100644
index 000000000..cb4647eeb
--- /dev/null
+++ b/micropython/drivers/storage/sdcard/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="SDCard block device driver.", version="0.1.0")
+
+module("sdcard.py", opt=3)
diff --git a/micropython/drivers/storage/sdcard/sdcard.py b/micropython/drivers/storage/sdcard/sdcard.py
new file mode 100644
index 000000000..c9c991f59
--- /dev/null
+++ b/micropython/drivers/storage/sdcard/sdcard.py
@@ -0,0 +1,306 @@
+"""
+MicroPython driver for SD cards using SPI bus.
+
+Requires an SPI bus and a CS pin.  Provides readblocks and writeblocks
+methods so the device can be mounted as a filesystem.
+
+Example usage on pyboard:
+
+    import pyb, sdcard, os
+    sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5)
+    pyb.mount(sd, '/sd2')
+    os.listdir('/')
+
+Example usage on ESP8266:
+
+    import machine, sdcard, os
+    sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15))
+    os.mount(sd, '/sd')
+    os.listdir('/')
+
+"""
+
+from micropython import const
+import time
+
+
+_CMD_TIMEOUT = const(100)
+
+_R1_IDLE_STATE = const(1 << 0)
+# R1_ERASE_RESET = const(1 << 1)
+_R1_ILLEGAL_COMMAND = const(1 << 2)
+# R1_COM_CRC_ERROR = const(1 << 3)
+# R1_ERASE_SEQUENCE_ERROR = const(1 << 4)
+# R1_ADDRESS_ERROR = const(1 << 5)
+# R1_PARAMETER_ERROR = const(1 << 6)
+_TOKEN_CMD25 = const(0xFC)
+_TOKEN_STOP_TRAN = const(0xFD)
+_TOKEN_DATA = const(0xFE)
+
+
+class SDCard:
+    def __init__(self, spi, cs, baudrate=1320000):
+        self.spi = spi
+        self.cs = cs
+
+        self.cmdbuf = bytearray(6)
+        self.dummybuf = bytearray(512)
+        self.tokenbuf = bytearray(1)
+        for i in range(512):
+            self.dummybuf[i] = 0xFF
+        self.dummybuf_memoryview = memoryview(self.dummybuf)
+
+        # initialise the card
+        self.init_card(baudrate)
+
+    def init_spi(self, baudrate):
+        try:
+            master = self.spi.MASTER
+        except AttributeError:
+            # on ESP8266
+            self.spi.init(baudrate=baudrate, phase=0, polarity=0)
+        else:
+            # on pyboard
+            self.spi.init(master, baudrate=baudrate, phase=0, polarity=0)
+
+    def init_card(self, baudrate):
+        # init CS pin
+        self.cs.init(self.cs.OUT, value=1)
+
+        # init SPI bus; use low data rate for initialisation
+        self.init_spi(100000)
+
+        # clock card at least 100 cycles with cs high
+        for i in range(16):
+            self.spi.write(b"\xff")
+
+        # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts)
+        for _ in range(5):
+            if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE:
+                break
+        else:
+            raise OSError("no SD card")
+
+        # CMD8: determine card version
+        r = self.cmd(8, 0x01AA, 0x87, 4)
+        if r == _R1_IDLE_STATE:
+            self.init_card_v2()
+        elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND):
+            self.init_card_v1()
+        else:
+            raise OSError("couldn't determine SD card version")
+
+        # get the number of sectors
+        # CMD9: response R2 (R1 byte + 16-byte block read)
+        if self.cmd(9, 0, 0, 0, False) != 0:
+            raise OSError("no response from SD card")
+        csd = bytearray(16)
+        self.readinto(csd)
+        if csd[0] & 0xC0 == 0x40:  # CSD version 2.0
+            self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024
+        elif csd[0] & 0xC0 == 0x00:  # CSD version 1.0 (old, <=2GB)
+            c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6
+            c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7
+            read_bl_len = csd[5] & 0b1111
+            capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len)
+            self.sectors = capacity // 512
+        else:
+            raise OSError("SD card CSD format not supported")
+        # print('sectors', self.sectors)
+
+        # CMD16: set block length to 512 bytes
+        if self.cmd(16, 512, 0) != 0:
+            raise OSError("can't set 512 block size")
+
+        # set to high data rate now that it's initialised
+        self.init_spi(baudrate)
+
+    def init_card_v1(self):
+        for i in range(_CMD_TIMEOUT):
+            time.sleep_ms(50)
+            self.cmd(55, 0, 0)
+            if self.cmd(41, 0, 0) == 0:
+                # SDSC card, uses byte addressing in read/write/erase commands
+                self.cdv = 512
+                # print("[SDCard] v1 card")
+                return
+        raise OSError("timeout waiting for v1 card")
+
+    def init_card_v2(self):
+        for i in range(_CMD_TIMEOUT):
+            time.sleep_ms(50)
+            self.cmd(58, 0, 0, 4)
+            self.cmd(55, 0, 0)
+            if self.cmd(41, 0x40000000, 0) == 0:
+                self.cmd(58, 0, 0, -4)  # 4-byte response, negative means keep the first byte
+                ocr = self.tokenbuf[0]  # get first byte of response, which is OCR
+                if not ocr & 0x40:
+                    # SDSC card, uses byte addressing in read/write/erase commands
+                    self.cdv = 512
+                else:
+                    # SDHC/SDXC card, uses block addressing in read/write/erase commands
+                    self.cdv = 1
+                # print("[SDCard] v2 card")
+                return
+        raise OSError("timeout waiting for v2 card")
+
+    def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False):
+        self.cs(0)
+
+        # create and send the command
+        buf = self.cmdbuf
+        buf[0] = 0x40 | cmd
+        buf[1] = arg >> 24
+        buf[2] = arg >> 16
+        buf[3] = arg >> 8
+        buf[4] = arg
+        buf[5] = crc
+        self.spi.write(buf)
+
+        if skip1:
+            self.spi.readinto(self.tokenbuf, 0xFF)
+
+        # wait for the response (response[7] == 0)
+        for i in range(_CMD_TIMEOUT):
+            self.spi.readinto(self.tokenbuf, 0xFF)
+            response = self.tokenbuf[0]
+            if not (response & 0x80):
+                # this could be a big-endian integer that we are getting here
+                # if final<0 then store the first byte to tokenbuf and discard the rest
+                if final < 0:
+                    self.spi.readinto(self.tokenbuf, 0xFF)
+                    final = -1 - final
+                for j in range(final):
+                    self.spi.write(b"\xff")
+                if release:
+                    self.cs(1)
+                    self.spi.write(b"\xff")
+                return response
+
+        # timeout
+        self.cs(1)
+        self.spi.write(b"\xff")
+        return -1
+
+    def readinto(self, buf):
+        self.cs(0)
+
+        # read until start byte (0xff)
+        for i in range(_CMD_TIMEOUT):
+            self.spi.readinto(self.tokenbuf, 0xFF)
+            if self.tokenbuf[0] == _TOKEN_DATA:
+                break
+            time.sleep_ms(1)
+        else:
+            self.cs(1)
+            raise OSError("timeout waiting for response")
+
+        # read data
+        mv = self.dummybuf_memoryview
+        if len(buf) != len(mv):
+            mv = mv[: len(buf)]
+        self.spi.write_readinto(mv, buf)
+
+        # read checksum
+        self.spi.write(b"\xff")
+        self.spi.write(b"\xff")
+
+        self.cs(1)
+        self.spi.write(b"\xff")
+
+    def write(self, token, buf):
+        self.cs(0)
+
+        # send: start of block, data, checksum
+        self.spi.read(1, token)
+        self.spi.write(buf)
+        self.spi.write(b"\xff")
+        self.spi.write(b"\xff")
+
+        # check the response
+        if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05:
+            self.cs(1)
+            self.spi.write(b"\xff")
+            return
+
+        # wait for write to finish
+        while self.spi.read(1, 0xFF)[0] == 0:
+            pass
+
+        self.cs(1)
+        self.spi.write(b"\xff")
+
+    def write_token(self, token):
+        self.cs(0)
+        self.spi.read(1, token)
+        self.spi.write(b"\xff")
+        # wait for write to finish
+        while self.spi.read(1, 0xFF)[0] == 0x00:
+            pass
+
+        self.cs(1)
+        self.spi.write(b"\xff")
+
+    def readblocks(self, block_num, buf):
+        # workaround for shared bus, required for (at least) some Kingston
+        # devices, ensure MOSI is high before starting transaction
+        self.spi.write(b"\xff")
+
+        nblocks = len(buf) // 512
+        assert nblocks and not len(buf) % 512, "Buffer length is invalid"
+        if nblocks == 1:
+            # CMD17: set read address for single block
+            if self.cmd(17, block_num * self.cdv, 0, release=False) != 0:
+                # release the card
+                self.cs(1)
+                raise OSError(5)  # EIO
+            # receive the data and release card
+            self.readinto(buf)
+        else:
+            # CMD18: set read address for multiple blocks
+            if self.cmd(18, block_num * self.cdv, 0, release=False) != 0:
+                # release the card
+                self.cs(1)
+                raise OSError(5)  # EIO
+            offset = 0
+            mv = memoryview(buf)
+            while nblocks:
+                # receive the data and release card
+                self.readinto(mv[offset : offset + 512])
+                offset += 512
+                nblocks -= 1
+            if self.cmd(12, 0, 0xFF, skip1=True):
+                raise OSError(5)  # EIO
+
+    def writeblocks(self, block_num, buf):
+        # workaround for shared bus, required for (at least) some Kingston
+        # devices, ensure MOSI is high before starting transaction
+        self.spi.write(b"\xff")
+
+        nblocks, err = divmod(len(buf), 512)
+        assert nblocks and not err, "Buffer length is invalid"
+        if nblocks == 1:
+            # CMD24: set write address for single block
+            if self.cmd(24, block_num * self.cdv, 0) != 0:
+                raise OSError(5)  # EIO
+
+            # send the data
+            self.write(_TOKEN_DATA, buf)
+        else:
+            # CMD25: set write address for first block
+            if self.cmd(25, block_num * self.cdv, 0) != 0:
+                raise OSError(5)  # EIO
+            # send the data
+            offset = 0
+            mv = memoryview(buf)
+            while nblocks:
+                self.write(_TOKEN_CMD25, mv[offset : offset + 512])
+                offset += 512
+                nblocks -= 1
+            self.write_token(_TOKEN_STOP_TRAN)
+
+    def ioctl(self, op, arg):
+        if op == 4:  # get number of blocks
+            return self.sectors
+        if op == 5:  # get block size in bytes
+            return 512
diff --git a/micropython/drivers/storage/sdcard/sdtest.py b/micropython/drivers/storage/sdcard/sdtest.py
new file mode 100644
index 000000000..ce700e2a8
--- /dev/null
+++ b/micropython/drivers/storage/sdcard/sdtest.py
@@ -0,0 +1,63 @@
+# Test for sdcard block protocol
+# Peter hinch 30th Jan 2016
+import machine
+import os
+import sdcard
+
+
+def sdtest():
+    spi = machine.SPI(1)
+    spi.init()  # Ensure right baudrate
+    sd = sdcard.SDCard(spi, machine.Pin.board.X21)  # Compatible with PCB
+    vfs = os.VfsFat(sd)
+    os.mount(vfs, "/fc")
+    print("Filesystem check")
+    print(os.listdir("/fc"))
+
+    line = "abcdefghijklmnopqrstuvwxyz\n"
+    lines = line * 200  # 5400 chars
+    short = "1234567890\n"
+
+    fn = "/fc/rats.txt"
+    print()
+    print("Multiple block read/write")
+    with open(fn, "w") as f:
+        n = f.write(lines)
+        print(n, "bytes written")
+        n = f.write(short)
+        print(n, "bytes written")
+        n = f.write(lines)
+        print(n, "bytes written")
+
+    with open(fn, "r") as f:
+        result1 = f.read()
+        print(len(result1), "bytes read")
+
+    fn = "/fc/rats1.txt"
+    print()
+    print("Single block read/write")
+    with open(fn, "w") as f:
+        n = f.write(short)  # one block
+        print(n, "bytes written")
+
+    with open(fn, "r") as f:
+        result2 = f.read()
+        print(len(result2), "bytes read")
+
+    os.umount("/fc")
+
+    print()
+    print("Verifying data read back")
+    success = True
+    if result1 == "".join((lines, short, lines)):
+        print("Large file Pass")
+    else:
+        print("Large file Fail")
+        success = False
+    if result2 == short:
+        print("Small file Pass")
+    else:
+        print("Small file Fail")
+        success = False
+    print()
+    print("Tests", "passed" if success else "failed")
diff --git a/micropython/espflash/espflash.py b/micropython/espflash/espflash.py
new file mode 100644
index 000000000..fbf4e1f7e
--- /dev/null
+++ b/micropython/espflash/espflash.py
@@ -0,0 +1,307 @@
+# This file is part of the MicroPython project, http://micropython.org/
+#
+# The MIT License (MIT)
+#
+# Copyright (c) 2022 Ibrahim Abdelkader <iabdalkader@openmv.io>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# A minimal esptool implementation to communicate with ESP32 ROM bootloader.
+# Note this tool does Not support advanced features, other ESP chips or stub loading.
+# This is only meant to be used for updating the U-blox Nina module firmware.
+
+import os
+import struct
+from micropython import const
+from time import sleep
+import binascii
+
+_CMD_SYNC = const(0x08)
+_CMD_CHANGE_BAUDRATE = const(0x0F)
+
+_CMD_ESP_READ_REG = const(0x0A)
+_CMD_ESP_WRITE_REG = const(0x09)
+
+_CMD_SPI_ATTACH = const(0x0D)
+_CMD_SPI_FLASH_MD5 = const(0x13)
+_CMD_SPI_FLASH_PARAMS = const(0x0B)
+_CMD_SPI_FLASH_BEGIN = const(0x02)
+_CMD_SPI_FLASH_DATA = const(0x03)
+_CMD_SPI_FLASH_END = const(0x04)
+
+_FLASH_ID = const(0)
+_FLASH_REG_BASE = const(0x60002000)
+_FLASH_BLOCK_SIZE = const(64 * 1024)
+_FLASH_SECTOR_SIZE = const(4 * 1024)
+_FLASH_PAGE_SIZE = const(256)
+
+_ESP_ERRORS = {
+    0x05: "Received message is invalid",
+    0x06: "Failed to act on received message",
+    0x07: "Invalid CRC in message",
+    0x08: "Flash write error",
+    0x09: "Flash read error",
+    0x0A: "Flash read length error",
+    0x0B: "Deflate error",
+}
+
+
+class ESPFlash:
+    def __init__(self, reset, gpio0, uart, log_enabled=False):
+        self.uart = uart
+        self.reset_pin = reset
+        self.gpio0_pin = gpio0
+        self.log = log_enabled
+        self.baudrate = 115200
+        self.md5sum = None
+        try:
+            import hashlib
+
+            if hasattr(hashlib, "md5"):
+                self.md5sum = hashlib.md5()
+        except ImportError:
+            pass
+
+    def _log(self, data, out=True):
+        if self.log:
+            size = len(data)
+            print(
+                f"out({size}) => " if out else f"in({size})  <= ",
+                "".join("%.2x" % (i) for i in data[0:10]),
+            )
+
+    def _uart_drain(self):
+        while self.uart.read(1) is not None:
+            pass
+
+    def _read_reg(self, addr):
+        v, d = self._command(_CMD_ESP_READ_REG, struct.pack("<I", _FLASH_REG_BASE + addr))
+        if d[0] != 0:
+            raise Exception("Command ESP_READ_REG failed.")
+        return v
+
+    def _write_reg(self, addr, data, mask=0xFFFFFFFF, delay=0):
+        v, d = self._command(
+            _CMD_ESP_WRITE_REG, struct.pack("<IIII", _FLASH_REG_BASE + addr, data, mask, delay)
+        )
+        if d[0] != 0:
+            raise Exception("Command ESP_WRITE_REG failed.")
+
+    def _poll_reg(self, addr, flag, retry=10, delay=0.050):
+        for i in range(retry):
+            reg = self._read_reg(addr)
+            if (reg & flag) == 0:
+                break
+            sleep(delay)
+        else:
+            raise Exception(f"Register poll timeout. Addr: 0x{addr:02X} Flag: 0x{flag:02X}.")
+
+    def _write_slip(self, pkt):
+        pkt = pkt.replace(b"\xdb", b"\xdb\xdd").replace(b"\xc0", b"\xdb\xdc")
+        self.uart.write(b"\xc0" + pkt + b"\xc0")
+        self._log(pkt)
+
+    def _read_slip(self):
+        pkt = None
+        # Find the packet start.
+        if self.uart.read(1) == b"\xc0":
+            pkt = bytearray()
+            while True:
+                b = self.uart.read(1)
+                if b is None or b == b"\xc0":
+                    break
+                pkt += b
+            pkt = pkt.replace(b"\xdb\xdd", b"\xdb").replace(b"\xdb\xdc", b"\xc0")
+            self._log(b"\xc0" + pkt + b"\xc0", False)
+        return pkt
+
+    def _strerror(self, err):
+        if err in _ESP_ERRORS:
+            return _ESP_ERRORS[err]
+        return "Unknown error"
+
+    def _checksum(self, data):
+        checksum = 0xEF
+        for i in data:
+            checksum ^= i
+        return checksum
+
+    def _command(self, cmd, payload=b"", checksum=0):
+        self._write_slip(struct.pack(b"<BBHI", 0, cmd, len(payload), checksum) + payload)
+        for i in range(10):
+            pkt = self._read_slip()
+            if pkt is not None and len(pkt) >= 8:
+                (flag, _cmd, size, val) = struct.unpack("<BBHI", pkt[:8])
+                if flag == 1 and cmd == _cmd:
+                    status = list(pkt[-4:])
+                    if status[0] == 1:
+                        raise Exception(f"Command {cmd} failed {self._strerror(status[1])}")
+                    return val, pkt[8:]
+        raise Exception(f"Failed to read response to command {cmd}.")
+
+    def set_baudrate(self, baudrate, timeout=350):
+        if not hasattr(self.uart, "init"):
+            return
+        if baudrate != self.baudrate:
+            print(f"Changing baudrate => {baudrate}")
+            self._uart_drain()
+            self._command(_CMD_CHANGE_BAUDRATE, struct.pack("<II", baudrate, 0))
+            self.baudrate = baudrate
+        self.uart.init(baudrate)
+        self._uart_drain()
+
+    def bootloader(self, retry=6):
+        for i in range(retry):
+            self.gpio0_pin(1)
+            self.reset_pin(0)
+            sleep(0.1)
+            self.gpio0_pin(0)
+            self.reset_pin(1)
+            sleep(0.1)
+            self.gpio0_pin(1)
+
+            if "POWERON_RESET" not in self.uart.read():
+                continue
+
+            for i in range(10):
+                self._uart_drain()
+                try:
+                    # 36 bytes: 0x07 0x07 0x12 0x20, followed by 32 x 0x55
+                    self._command(_CMD_SYNC, b"\x07\x07\x12\x20" + 32 * b"\x55")
+                    self._uart_drain()
+                    return True
+                except Exception as e:
+                    print(e)
+
+        raise Exception("Failed to enter download mode!")
+
+    def flash_read_size(self):
+        SPI_REG_CMD = 0x00
+        SPI_USR_FLAG = 1 << 18
+        SPI_REG_USR = 0x1C
+        SPI_REG_USR2 = 0x24
+        SPI_REG_W0 = 0x80
+        SPI_REG_DLEN = 0x2C
+
+        # Command bit len | command
+        SPI_RDID_CMD = ((8 - 1) << 28) | 0x9F
+        SPI_RDID_LEN = 24 - 1
+
+        # Save USR and USR2 registers
+        reg_usr = self._read_reg(SPI_REG_USR)
+        reg_usr2 = self._read_reg(SPI_REG_USR2)
+
+        # Enable command phase and read phase.
+        self._write_reg(SPI_REG_USR, (1 << 31) | (1 << 28))
+
+        # Configure command.
+        self._write_reg(SPI_REG_DLEN, SPI_RDID_LEN)
+        self._write_reg(SPI_REG_USR2, SPI_RDID_CMD)
+
+        self._write_reg(SPI_REG_W0, 0)
+        # Trigger SPI operation.
+        self._write_reg(SPI_REG_CMD, SPI_USR_FLAG)
+
+        # Poll CMD_USER flag.
+        self._poll_reg(SPI_REG_CMD, SPI_USR_FLAG)
+
+        # Restore USR and USR2 registers
+        self._write_reg(SPI_REG_USR, reg_usr)
+        self._write_reg(SPI_REG_USR2, reg_usr2)
+
+        flash_bits = int(self._read_reg(SPI_REG_W0)) >> 16
+        if flash_bits < 0x12 or flash_bits > 0x19:
+            raise Exception(f"Unexpected flash size bits: 0x{flash_bits:02X}.")
+
+        flash_size = 2**flash_bits
+        print(f"Flash size {flash_size / 1024 / 1024} MBytes")
+        return flash_size
+
+    def flash_attach(self):
+        self._command(_CMD_SPI_ATTACH, struct.pack("<II", 0, 0))
+        print("Flash attached")
+
+    def flash_config(self, flash_size=2 * 1024 * 1024):
+        self._command(
+            _CMD_SPI_FLASH_PARAMS,
+            struct.pack(
+                "<IIIIII",
+                _FLASH_ID,
+                flash_size,
+                _FLASH_BLOCK_SIZE,
+                _FLASH_SECTOR_SIZE,
+                _FLASH_PAGE_SIZE,
+                0xFFFF,
+            ),
+        )
+
+    def flash_write_file(self, path, blksize=0x1000):
+        size = os.stat(path)[6]
+        total_blocks = (size + blksize - 1) // blksize
+        erase_blocks = 1
+        print(f"Flash write size: {size} total_blocks: {total_blocks} block size: {blksize}")
+        with open(path, "rb") as f:
+            seq = 0
+            for i in range(total_blocks):
+                buf = f.read(blksize)
+                # Update digest
+                if self.md5sum is not None:
+                    self.md5sum.update(buf)
+                # The last data block should be padded to the block size with 0xFF bytes.
+                if len(buf) < blksize:
+                    buf += b"\xff" * (blksize - len(buf))
+                checksum = self._checksum(buf)
+                if seq % erase_blocks == 0:
+                    # print(f"Erasing {seq} -> {seq+erase_blocks}...")
+                    self._command(
+                        _CMD_SPI_FLASH_BEGIN,
+                        struct.pack(
+                            "<IIII", erase_blocks * blksize, erase_blocks, blksize, seq * blksize
+                        ),
+                    )
+                print(f"Writing sequence number {seq}/{total_blocks}...")
+                self._command(
+                    _CMD_SPI_FLASH_DATA,
+                    struct.pack("<IIII", len(buf), seq % erase_blocks, 0, 0) + buf,
+                    checksum,
+                )
+                seq += 1
+
+        print("Flash write finished")
+
+    def flash_verify_file(self, path, digest=None, offset=0):
+        if digest is None:
+            if self.md5sum is None:
+                raise Exception("MD5 checksum missing.")
+            digest = binascii.hexlify(self.md5sum.digest())
+
+        size = os.stat(path)[6]
+        val, data = self._command(_CMD_SPI_FLASH_MD5, struct.pack("<IIII", offset, size, 0, 0))
+
+        print(f"Flash verify: File  MD5 {digest}")
+        print(f"Flash verify: Flash MD5 {bytes(data[0:32])}")
+
+        if digest == data[0:32]:
+            print("Firmware verified.")
+        else:
+            raise Exception("Firmware verification failed.")
+
+    def reboot(self):
+        payload = struct.pack("<I", 0)
+        self._write_slip(struct.pack(b"<BBHI", 0, _CMD_SPI_FLASH_END, len(payload), 0) + payload)
diff --git a/micropython/espflash/example.py b/micropython/espflash/example.py
new file mode 100644
index 000000000..76e8eb84e
--- /dev/null
+++ b/micropython/espflash/example.py
@@ -0,0 +1,29 @@
+import espflash
+from machine import Pin
+from machine import UART
+
+if __name__ == "__main__":
+    reset = Pin(3, Pin.OUT)
+    gpio0 = Pin(2, Pin.OUT)
+    uart = UART(1, 115200, tx=Pin(8), rx=Pin(9), timeout=350)
+
+    md5sum = b"9a6cf1257769c9f1af08452558e4d60e"
+    path = "NINA_W102-v1.5.0-Nano-RP2040-Connect.bin"
+
+    esp = espflash.ESPFlash(reset, gpio0, uart)
+    # Enter bootloader download mode, at 115200
+    esp.bootloader()
+    # Can now chage to higher/lower baudrate
+    esp.set_baudrate(921600)
+    # Must call this first before any flash functions.
+    esp.flash_attach()
+    # Read flash size
+    size = esp.flash_read_size()
+    # Configure flash parameters.
+    esp.flash_config(size)
+    # Write firmware image from internal storage.
+    esp.flash_write_file(path)
+    # Compares file and flash MD5 checksum.
+    esp.flash_verify_file(path, md5sum)
+    # Resets the ESP32 chip.
+    esp.reboot()
diff --git a/micropython/espflash/manifest.py b/micropython/espflash/manifest.py
new file mode 100644
index 000000000..e6b639924
--- /dev/null
+++ b/micropython/espflash/manifest.py
@@ -0,0 +1,6 @@
+metadata(
+    version="0.1.0",
+    description="Provides a minimal ESP32 bootloader protocol implementation.",
+)
+
+module("espflash.py")
diff --git a/micropython/lora/README.md b/micropython/lora/README.md
new file mode 100644
index 000000000..28a05483e
--- /dev/null
+++ b/micropython/lora/README.md
@@ -0,0 +1,1213 @@
+# LoRa driver
+
+This MicroPython library provides synchronous and asynchronous wireless drivers
+for Semtech's LoRa (Long Range Radio) modem devices.
+
+(LoRa is a registered trademark or service mark of Semtech Corporation or its
+affiliates.)
+
+## Support
+
+Currently these radio modem chipsets are supported:
+
+* SX1261
+* SX1262
+* SX1276
+* SX1277
+* SX1278
+* SX1279
+* STM32WL55 "sub-GHz radio" peripheral
+
+Most radio configuration features are supported, as well as transmitting or
+receiving packets.
+
+This library can be used on any MicroPython port which supports the `machine.SPI`
+interface.
+
+## Installation
+
+First, install at least one of the following "base" LoRa packages:
+
+- `lora-sync` to use the synchronous LoRa modem API.
+- `lora-async` to use the asynchronous LoRa modem API with
+  [asyncio](https://docs.micropython.org/en/latest/library/asyncio.html). Support
+  for `asyncio` must be included in your MicroPython build to use `lora-async`.
+
+Second, install at least one of the following modem chipset drivers for the
+modem model that matches your hardware:
+
+- `lora-sx126x` for SX1261 & SX1262 support.
+- `lora-sx127x` for SX1276-SX1279 support.
+- `lora-stm32wl5` for STM32WL55 support.
+
+It's recommended to install only the packages that you need, to save firmware
+size.
+
+Installing any of these packages will automatically also install a common
+base package, `lora`.
+
+For more information about how to install packages, or "freeze" them into a
+firmware image, consult the [MicroPython documentation on "Package
+management"](https://docs.micropython.org/en/latest/reference/packages.html).
+
+## Initializing Driver
+
+### Creating SX1262 or SX1261
+
+This is the synchronous modem class, and requires `lora-sync` to be installed:
+
+```py
+from machine import SPI, Pin
+import lora import SX1262  # or SX1261, depending on which you have
+
+def get_modem():
+    # The LoRa configuration will depend on your board and location, see
+    # below under "Modem Configuration" for some possible examples.
+    lora_cfg = { 'freq_khz': SEE_BELOW_FOR_CORRECT_VALUE }
+
+    # To instantiate SPI correctly, see
+    # https://docs.micropython.org/en/latest/library/machine.SPI.html
+    spi = SPI(0, baudrate=2000_000)
+    cs = Pin(9)
+
+    # or SX1261(), depending on which you have
+    return SX1262(spi, cs,
+                 busy=Pin(2),  # Required
+                 dio1=Pin(20),   # Optional, recommended
+                 reset=Pin(15),  # Optional, recommended
+                 lora_cfg=lora_cfg)
+
+modem = get_modem()
+```
+
+### Creating SX127x
+
+This is the synchronous modem class, and requires `lora-sync` to be installed:
+
+```py
+from machine import SPI, Pin
+# or SX1277, SX1278, SX1279, depending on which you have
+from lora import SX1276
+
+def get_modem():
+    # The LoRa configuration will depend on your board and location, see
+    # below under "Modem Configuration" for some possible examples.
+    lora_cfg = { 'freq_khz': SEE_BELOW_FOR_CORRECT_VALUE }
+
+    # To instantiate SPI correctly, see
+    # https://docs.micropython.org/en/latest/library/machine.SPI.html
+    spi = SPI(0, baudrate=2000_000)
+    cs = Pin(9)
+
+    # or SX1277, SX1278, SX1279, depending on which you have
+    return SX1276(spi, cs,
+                  dio0=Pin(10),  # Optional, recommended
+                  dio1=Pin(11),  # Optional, recommended
+                  reset=Pin(13),  # Optional, recommended
+                  lora_cfg=lora_cfg)
+
+modem = get_modem()
+```
+
+*Note*: Because SX1276, SX1277, SX1278 and SX1279 are very similar, currently
+the driver uses the same code for any. Dealing with per-part limitations (for
+example: lower max frequency, lower maximum SF value) is responsibility of the
+calling code. When possible please use the correct class anyhow, as per-part
+code may be added in the future.
+
+### Creating STM32WL55
+
+```
+from lora import WL55SubGhzModem
+
+def get_modem():
+    # The LoRa configuration will depend on your board and location, see
+    # below under "Modem Configuration" for some possible examples.
+    lora_cfg = { 'freq_khz': SEE_BELOW_FOR_CORRECT_VALUE }
+    return WL55SubGhzModem(lora_cfg)
+
+modem = get_modem()
+```
+
+Note: As this is an internal peripheral of the STM32WL55 microcontroller,
+support also depends on MicroPython being built for a board based on this
+microcontroller.
+
+### Notes about initialisation
+
+* See below for details about the `lora_cfg` structure that configures the modem's
+  LoRa registers.
+* Connecting radio "dio" pins as shown above is optional but recommended so the
+  driver can use pin interrupts for radio events. If not, the driver needs to
+  poll the chip instead. Interrupts allow reduced power consumption and may also
+  improve receive sensitivity (by removing SPI bus noise during receive
+  operations.)
+
+### All constructor parameters
+
+Here is a full list of parameters that can be passed to both constructors:
+
+#### S1261/SX1262
+
+(Note: It's important to instantiate the correct object as these two modems have small differences in their command protocols.)
+
+| Parameter                 | Required               | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
+|---------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `spi`                     | Yes                    | Instance of a `machine.SPI` object or compatible, for the modem's SPI interface (modem MISO, MOSI, SCK pins).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
+| `cs`                      | Yes                    | Instance of a `machine.Pin` input, as connected to the modem's NSS pin.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
+| `busy`                    | Yes                    | Instance of a `machine.Pin` input, as connected to the modem's BUSY pin.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
+| `dio1`                    | No                     | Instance of a `machine.Pin` input, as connected to the modem's DIO1 pin. If not provided then interrupts cannot be used to detect radio events.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
+| `dio2_rf_sw`              | No, defaults to `True` | By default, configures the modem's DIO2 pin as an RF switch. The modem will drive this pin high when transmitting and low otherwise. Set this parameter to False if DIO2 is connected elsewhere on your LoRa board/module and you don't want it toggling on transmit.                                                                                                                                                                                                                                                                                                                                                 |
+| `dio3_tcxo_millivolts`    | No                     | If set to an integer value, DIO3 will be used as a variable voltage source for the modem's main TCXO clock source. DIO3 will automatically disable the TCXO to save power when the 32MHz clock source is not needed. The value is units of millivolts and should be one of the voltages listed in the SX1261 datasheet section 13.3.6 "SetDIO3AsTCXOCtrl". Any value between `1600` and `3300` can be specified and the driver will round down to a lower supported voltage step if necessary. The manufacturer of the LoRa board or module you are using should be able to tell you what value to pass here, if any. |
+| `dio3_tcxo_start_time_us` | No                     | This value is ignored unless `dio3_tcxo_millivolts` is set, and is the startup delay in microseconds for the TCXO connected to DIO3. Each time the modem needs to enable the TCXO, it will wait this long. The default value is `1000` (1ms). Values can be set in multiples of `15.625`us and range from 0us to 262 seconds (settings this high will make the modem unusable).                                                                                                                                                                                                                                           |
+| reset               | No                     | If set to a `machine.Pin` output attached to the modem's NRESET pin , then it will be used to hard reset the modem before initializing it. If unset, the programmer is responsible for ensuring the modem is in an idle state when the constructor is called.                                                                                                                                                                                                                                                                                                                                                         |
+| `lora_cfg`                | No                     | If set to an initial LoRa configuration then the modem is set up with this configuration. If not set here, can be set by calling `configure()` later on.                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
+| `ant_sw`                  | No                     | Optional antenna switch object instance, see below for description.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
+
+
+#### SX1276/SX1277/SX1278/SX1279
+
+| Parameter   | Required | Description                                                                                                                                                                                                                                                      |   |
+|-------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---|
+| `spi`         | Yes      | Instance of a `machine.SPI` object or compatible, for the modem's SPI interface (modem MISO, MOSI, SCK pins).                                                                                                                                                    |   |
+| `cs`          | Yes      | Instance of a `machine.Pin` input, as connected to the modem's NSS pin.                                                                                                                                                                                          |   |
+| `dio0`        | No       | Instance of a `machine.Pin` input, as connected to the modem's DIO0 pin. If set, allows the driver to use interrupts to detect "RX done" and "TX done" events.                                                                                                   |   |
+| `dio1`        | No       | Instance of a `machine.Pin` input, as connected to the modem's DIO1/DCLK pin. If set, allows the driver to use interrupts to detect "RX timeout" events. Setting this pin requires dio0 to also be set.                                                          |   |
+| `reset` | No       | If set to a `machine.Pin` output attached the modem's NRESET pin , it will be used to hard reset the modem before initializing it. If unset, the programmer is responsible for ensuring the modem should be is in an idle state when the object is instantiated. |   |
+| `lora_cfg`    | No       | If set to an initial LoRa configuration then the modem is set up with this configuration. If not set here, can be set by calling `configure()` later on.                                                                                                         |   |
+| `ant`_sw      | No       | Optional antenna switch object instance, see below for description.                                                                                                                                                                                              |   |
+
+#### STM32WL55
+
+| Parameter         | Required | Description                                                                                                                                                                    |
+|-------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `lora_cfg`        | No       | If set to an initial LoRa configuration then the modem is set up with this configuration. If not set here, can be set by calling `configure()` later on.                       |
+| `tcxo_millivolts` | No       | Defaults to 1700. The voltage supplied on pin PB0_VDDTCXO. See `dio3_tcxo_millivolts` above for details, this parameter has the same behaviour.                                |
+| ant_sw            | No       | Defaults to an instance of `lora.NucleoWL55RFConfig` class for the NUCLEO-WL55 development board. Set to `None` to disable any automatic antenna switching. See below for description.                               |
+
+
+## Modem Configuration
+
+It is necessary to correctly configure the modem before use. At minimum, the
+correct RF frequency must be set. There are many additional LoRa modem radio
+configuration settings that may be important. For two LoRa modem modems to
+communicate, their radio configurations must be compatible.
+
+**Different regions in the world also have RF regulations which you must abide
+by. Check RF communications regulations for the location you are in, to
+determine which configurations are legal for you to use.**
+
+Modem configuration can be set in two ways:
+
+- Pass `lora_cfg` keyword parameter to the modem class constructor (see examples
+  above).
+- Call `modem.configure(lora_cfg)` at any time
+
+Where `lora_cfg` is a `dict` containing configuration keys and values. If a key
+is missing, the value set in the modem is unchanged.
+
+### Basic Configuration
+
+The minimal configuration is the modem frequency:
+
+```py
+lora_cfg = { 'freq_khz': 916000 }
+modem.configure(lora_cfg)
+```
+
+The example above sets the main frequency to 916.0MHz (916,000kHz), and leaves
+the rest of the modem settings at their defaults. If you have two of the same
+module using this driver, setting the same frequency on both like this may be
+enough for them to communicate. However, the other default settings are a
+compromise that may not give the best range, bandwidth, reliability, etc. for
+your application. The defaults may not even be legal to transmit with in your
+region!
+
+Other parameters, particularly Bandwidth (`lora_cfg["bw"]`), Spreading Factor
+(`lora_cfg["sf"]`), and transmit power (`lora_cfg["output_power"]`) may be
+limited in your region. You should find this out as well and ensure the
+configuration you are using is allowed. **This is your responsibility!**
+
+#### Defaults
+
+If you don't configure anything, the default settings are equivalent to:
+
+```py
+lora_cfg = { 'freq_khz': None, # Must set this
+             'sf': 7,
+             'coding_rate': 5,  # 4/5 Coding
+             'bw': '125',
+             }
+```
+
+With the default `output_power` level depending on the radio in use.
+
+#### Choosing Other Parameters
+
+Valid choices are determined by two things:
+
+- Regulatory rules in your region. This information is provided by regional
+  authorities, but it may also be useful to consult the [LoRaWAN Regional
+  Parameters document
+  (official)](https://resources.lora-alliance.org/technical-specifications/rp002-1-0-4-regional-parameters)
+  and the ([Things Network
+  (unofficial)](https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/)
+  for LoRaWAN frequency plans.
+
+  Even if you're not connecting to a LoRaWAN network, if you choose frequency
+  and bandwidth settings from the LoRaWAN recommendations for your region then
+  they should be legal to use.
+- Design of the radio module/modem/board you are using. RF antenna components
+  are usually tailored for particular frequency ranges. One some boards only
+  particular antenna ports or other features may be connected.
+
+#### Longer Range Configuration
+
+Here is an example aiming for higher range and lower data rate with the main
+frequency set again to 916Mhz (for the "AU915" Australian region):
+
+```py
+lora_cfg = { 'freq_khz': 916000,
+             'sf': 12,
+             'bw': '62.5',  # kHz
+             'coding_rate': 8,
+             'output_power': 20,  # dBm
+             'rx_boost': True,
+         }
+```
+
+Quick explanation of these settings, for more detailed explanations see the next
+section below:
+
+* Setting `sf` to maximum (higher "Spreading Factor") means each LoRa "chirp"
+  takes longer, for more range but lower data rate.
+* Setting `bw` bandwidth setting lower makes the signal less susceptible to
+  noise, but again slower. 62.5kHz is the lowest setting recommended by Semtech
+  unless the modem uses a TCXO for high frequency stability, rather than a
+  cheaper crystal.
+* Setting `coding_rate` higher to 4/8 means that more Forward Error Correction
+  information is sent, slowing the data rate but increasing the chance of a
+  packet being received correctly.
+* Setting `output_power` to 20dBm will select the maximum (or close to it) for
+  the radio, which may be less than 20dBm.
+* Enabling `rx_boost` will increase the receive sensitivity of the radio, if
+  it supports this.
+
+<details>
+<summary>Additional Australia-specific regulatory explanation</summary>
+
+The LoRaWAN AU915 specifications suggest 125kHz bandwidth. To tell that it's OK
+to set `bw` lower, consult the Australian [Low Interference Potential Devices
+class license](https://www.legislation.gov.au/Series/F2015L01438). This class
+license allows Digital Modulation Transmitters in the 915-928MHz band to
+transmit up to 1W Maximum EIRP provided "*The radiated peak power spectral
+density in any 3 kHz must not exceed 25 mW per 3 kHz*".
+
+`output_power` set to 20dBm is 100mW, over 62.5kHz bandwidth gives
+1.6mW/kHz. This leaves significant headroom for antenna gain that might increase
+radiated power in some directions.)
+</details>
+
+### Configuration Keys
+
+These keys can be set in the `lora_cfg` dict argument to `configure()`,
+and correspond to the parameters documented in this section.
+
+Consult the datasheet for the LoRa modem you are using for an in-depth
+description of each of these parameters.
+
+Values which are unset when `configure()` is called will keep their existing
+values.
+
+#### `freq_khz` - RF Frequency
+Type: `int` (recommended) or `float` (if supported by port)
+
+LoRa RF frequency in kHz. See above for notes about regulatory limits on this
+value.
+
+The antenna and RF matching components on a particular LoRa device may only
+support a particular frequency range. Consult the manufacturer's documentation.
+
+#### `sf` - Spreading Factor
+Type: `int`
+
+Spreading Factor, numeric value only. Higher spreading factors allow reception
+of weaker signals but have slower data rate.
+
+The supported range of SF values varies depending on the modem chipset:
+
+| Spreading Factor | Supported SX126x | Supported SX127x      |
+|------------------|------------------|-----------------------|
+| 5                | Yes              | **No**                |
+| 6                | **Yes** [*]      | **Yes** [*]           |
+| 7                | Yes              | Yes                   |
+| 8                | Yes              | Yes                   |
+| 9                | Yes              | Yes                   |
+| 10               | Yes              | Yes, except SX1277[^] |
+| 11               | Yes              | Yes, except SX1277[^] |
+| 12               | Yes              | Yes, except SX2177[^] |
+
+[*] SF6 is not compatible between SX126x and SX127x chipsets.
+
+[^] SX1276, SX1278 and SX1279 all support SF6-SF12. SX1277 only supports
+SF6-SF9. This limitation is not checked by the driver.
+
+#### `bw` - Bandwidth
+Type: `int` or `str`
+
+Default: 125
+
+Bandwidth value in kHz. Must be exactly one of these LoRa bandwidth values:
+
+* 7.8
+* 10.4
+* 15.6
+* 20.8
+* 31.25
+* 41.7
+* 62.5
+* 125
+* 250
+* 500
+
+Higher bandwidth transmits data faster and reduces peak spectral density when
+transmitting, but is more susceptible to interference.
+
+IF setting bandwidth below 62.5kHz then Semtech recommends using a hardware TCXO
+as the modem clock source, not a cheaper crystal. Consult the modem datasheet
+and your hardware maker's reference for more information and to determine which
+clock source your LoRa modem hardware is using.
+
+For non-integer bandwidth values, it's recommended to always set this parameter
+as a `str` (i.e. `"15.6"`) not a numeric `float`.
+
+#### `coding_rate` - FEC Coding Rate
+Type: `int`
+
+Default: 5
+
+Forward Error Correction (FEC) coding rate is expressed as a ratio, `4/N`. The
+value passed in the configuration is `N`:
+
+| Value | Error coding rate |
+|-------|-------------------|
+| 5     | 4/5               |
+| 6     | 4/6               |
+| 7     | 4/7               |
+| 8     | 4/8               |
+
+Setting a higher value makes transmission slower but increases the chance of
+receiving successfully in a noisy environment
+
+In explicit header mode (the default), `coding_rate` only needs to be set by the
+transmitter and the receiver will automatically choose the correct rate when
+receiving based on the received header. In implicit header mode (see
+`implicit_header`), this value must be set the same on both transmitter and
+receiver.
+
+#### `tx_ant` - TX Antenna
+Supported: *SX127x only*.
+
+Type: `str`, not case sensitive
+
+Default: RFO_HF or RFO_LF (low power)
+
+SX127x modems and STM32WL55 microcontrollers have multiple antenna pins for
+different power levels and frequency ranges. The board/module that the LoRa
+modem chip is on may have particular antenna connections, or even an RF switch
+that needs to be set via a GPIO to connect an antenna pin to a particular output
+(see `ant_sw`, below).
+
+The driver must configure the modem to use the correct pin for a particular
+hardware antenna connection before transmitting. When receiving, the modem
+chooses the correct pin based on the selected frequency.
+
+A common symptom of incorrect `tx_ant` setting is an extremely weak RF signal.
+
+Consult modem datasheet for more details.
+
+##### SX127x tx_ant
+
+| Value           | RF Transmit Pin                  |
+|-----------------|----------------------------------|
+| `"PA_BOOST"`    | PA_BOOST pin (high power)        |
+| Any other value | RFO_HF or RFO_LF pin (low power) |
+
+Pin "RFO_HF" is automatically used for frequencies above 862MHz, and is not
+supported on SX1278. "RFO_LF" is used for frequencies below 862MHz. Consult
+datasheet Table 32 "Frequency Bands" for more details.
+
+##### WL55SubGhzModem tx_ant
+
+| Value           | RF Transmit Pin         |
+|-----------------|-------------------------|
+| `"PA_BOOST"`    | RFO_HP pin (high power) |
+| Any other value | RFO_LP pin (low power)  |
+
+**NOTE**: Currently the `PA_BOOST` HP antenna output is lower than it should be
+on this board, due to an unknown driver bug.
+
+If setting `tx_ant` value, also set `output_power` at the same time or again
+before transmitting.
+
+#### `output_power` - Transmit output power level
+Type: `int`
+
+Default: Depends on modem
+
+Nominal TX output power in dBm. The possible range depends on the modem and for
+some modems the `tx_ant` configuration.
+
+| Modem           | `tx_ant` value             | Range (dBm)       | "Optimal" (dBm)        |   |
+|-----------------|----------------------------|-------------------|------------------------|---|
+| SX1261          | N/A                        | -17 to +15        | +10, +14 or +15 [*][^] |   |
+| SX1262          | N/A                        | -9 to +22         | +14, +17, +20, +22 [*] |   |
+| SX127x          | "PA_BOOST"                 | +2 to +17, or +20 | Any                    |   |
+| SX127x          | RFO_HF or RFO_LF           | -4 to +15         | Any                    |   |
+| WL55SubGhzModem | "PA_BOOST"                 | -9 to +22         | +14, +17, +20, +22 [*] |   |
+| WL55SubGhzModem | Any other value (not None) | -17 to +14        | +10, +14 or +15 [*][^] |   |
+
+Values which are out of range for the modem will be clamped at the
+minimum/maximum values shown above.
+
+Actual radiated TX power for RF regulatory purposes depends on the RF hardware,
+antenna, and the rest of the modem configuration. It should be measured and
+tuned empirically not determined from this configuration information alone.
+
+[*] For some modems the datasheet shows "Optimal" Power Amplifier
+configuration values for these output power levels. If setting one of these
+levels, the optimal settings from the datasheet are applied automatically by the
+driver. Therefore it is recommended to use one of these power levels if
+possible.
+
+[^] In the marked configurations +15dBm is only possible with frequency above
+400MHz, will be +14dBm otherwise.
+
+#### `implicit_header` - Implicit/Explicit Header Mode
+Type: `bool`
+
+Default: `False`
+
+LoRa supports both implicit and explicit header modes. Explicit header mode
+(`implicit_header` set to False) is the default.
+
+`implicit_header` must be set the same on both sender and receiver.
+
+* In explicit header mode (default), each transmitted LoRa packet has a header
+  which contains information about the payload length, `coding_rate` value in
+  use, and whether the payload has a CRC attached (`crc_en`). The receiving
+  modem decodes and verifies the header and uses the values to receive the
+  correct length payload and verify the CRC if enabled.
+* In implicit header mode (`implicit_header` set to True), this header is not
+  sent and this information must be already be known and configured by both
+  sender and receiver. Specifically:
+  - `crc_en` setting should be set the same on both sender and receiver.
+  - `coding_rate` setting must match between the sender and receiver.
+  - Receiver must provide the `rx_length` argument when calling either
+    `recv()` or `start_recv()`. This length must match the length in bytes
+    of the payload sent by the sender.
+
+### `crc_en` - Enable CRCs
+Type: `bool`
+
+Default: `True`
+
+LoRa packets can have a 16-bit CRC attached to determine if a packet is
+received correctly without corruption.
+
+* In explicit header mode (default), the sender will attach a CRC if
+  `crc_en` is True. `crc_en` parameter is ignored by the receiver, which
+  determines if there is a CRC based on the received header and will check it if
+  so.
+* In implicit header mode, the sender will only include a CRC if `crc_en`
+  is True and the receiver will only check the CRC if `crc_en` is True.
+
+By default, if CRC checking is enabled on the receiver then the LoRa modem driver
+silently drops packets with invalid CRCs. Setting `modem.rx_crc_error = True`
+will change this so that packets with failed CRCs are returned to the caller,
+with the `crc_error` field set to True (see `RxPacket`, below).
+
+#### `auto_image_cal` - Automatic Image Calibration
+Supported: *SX127x only*.
+
+Type: `bool`
+
+Default: `False`
+
+If set True, enable automatic image re-calibration in the modem if the
+temperature changes significantly. This may avoid RF performance issues caused
+by frequency drift, etc. Setting this value may lead to dropped packets received
+when an automatic calibration event is in progress.
+
+Consult SX127x datasheet for more information.
+
+#### `syncword` - Sync Word
+Type: `int`
+
+Default: `0x12`
+
+LoRa Sync Words are used to differentiate LoRa packets as being for Public or
+Private networks. Sync Word must match between sender and receiver.
+
+For SX127x this value is an 8-bit integer. Supported values 0x12 for Private
+Networks (default, most users) and 0x34 for Public Networks (LoRaWAN only).
+
+For SX126x this value is a 16-bit integer. Supported values 0x1424 for Private
+
+Networks (default, most users) and 0x3444 for Public Networks. However the
+driver will automatically [translate values configured using the 8-bit SX127x
+format](https://www.thethingsnetwork.org/forum/t/should-private-lorawan-networks-use-a-different-sync-word/34496/15)
+for software compatibility, so setting an 8-bit value is supported on all modems.
+
+You probably shouldn't change this value from the default, unless connecting to
+a LoRaWAN network.
+
+#### `pa_ramp_us` - PA Ramp Time
+Type: `int`
+
+Default: `40`us
+
+Power Amplifier ramp up/down time, as expressed in microseconds.
+
+The exact values supported on each radio are different. Configuring an
+unsupported value will cause the driver to choose the next highest value that is
+supported for that radio.
+
+| Value (us) | Supported SX126x | Supported SX127x |
+|------------|------------------|------------------|
+| 10         | Yes              | Yes              |
+| 12         | No               | Yes              |
+| 15         | No               | Yes              |
+| 20         | Yes              | Yes              |
+| 25         | No               | Yes              |
+| 31         | No               | Yes              |
+| 40         | Yes              | Yes              |
+| 50         | No               | Yes              |
+| 62         | No               | Yes              |
+| 80         | Yes              | No               |
+| 100        | No               | Yes              |
+| 125        | No               | Yes              |
+| 200        | Yes              | No               |
+| 250        | No               | Yes              |
+| 500        | No               | Yes              |
+| 800        | Yes              | No               |
+| 1000       | No               | Yes              |
+| 1700       | Yes              | No               |
+| 2000       | No               | Yes              |
+| 3400       | Yes              | Yes              |
+
+#### `preamble_len` - Preamble Length
+Type: `int`
+Default: `12`
+
+Length of the preamble sequence, in units of symbols.
+
+#### `invert_iq_tx`/`invert_iq_rx` - Invert I/Q
+Type: `bool`
+
+Default: Both `False`
+
+If `invert_iq_tx` or `invert_iq_rx` is set then IQ polarity is inverted in the
+radio for either TX or RX, respectively. The receiver's `invert_iq_rx` setting
+must match the sender's `invert_iq_tx` setting.
+
+This is necessary for LoRaWAN where end-devices transmit with inverted IQ
+relative to gateways.
+
+Note: The current SX127x datasheet incorrectly documents the modem register
+setting corresponding to `invert_iq_tx`. This driver configures TX polarity
+correctly for compatibility with other LoRa modems, most other SX127x drivers,
+and LoRaWAN. However, there are some SX127x drivers that follow the datasheet
+description, and they will set `invert_iq_tx` opposite to this.
+
+#### `rx_boost` - Boost receive sensitivity
+Type: `bool`
+
+Default: `False`
+
+Enable additional receive sensitivity if available.
+
+* On SX126x, this makes use of the "Rx Boosted gain" option.
+* On SX127x, this option is available for HF bands only and sets the LNA boost
+  register field.
+
+#### `lna_gain` - Receiver LNA gain
+Supported: *SX127x only*.
+
+Type: `int` or `None`
+
+Default: `1`
+
+Adjust the LNA gain level for receiving. Valid values are `None` to enable
+Automatic Gain Control, or integer gain levels 1 to 6 where 1 is maximum gain
+(default).
+
+## Sending & Receiving
+
+### Simple API
+
+The driver has a "simple" API to easily send and receive LoRa packets. The
+API is fully synchronous, meaning the caller is blocked until the LoRa operation
+(send or receive) is done. The Simple API doesn't support starting a
+send while a receive in progress (or vice versa). It is suitable for simple
+applications only.
+
+For an example that uses the simple API, see `examples/reliable_delivery/sender.py`.
+
+#### send
+
+To send (transmit) a LoRa packet using the configured modulation settings:
+
+```py
+def send(self, packet, tx_at_ms=None)
+```
+
+Example:
+
+```py
+modem.send(b'Hello world')
+```
+
+* `send()` transmits a LoRa packet with the provided payload bytes, and returns
+  once transmission is complete.
+* The return value is the timestamp when transmission completed, as a
+  `time.ticks_ms()` result. It will be more accurate if the modem was
+  initialized to use interrupts.
+
+For precise timing of sent packets, there is an optional `tx_at_ms` argument
+which is a timestamp (as a `time.ticks_ms()` value). If set, the packet will be
+sent as close as possible to this timestamp and the function will block until
+that time arrives:
+
+```py
+modem.send(b'Hello world', time.ticks_add(time.ticks_ms(), 250))
+```
+
+(This allows more precise timing of sent packets, without needing to account for
+the length of the packet to be copied to the modem.)
+
+### receive
+
+```py
+def recv(self, timeout_ms=None, rx_length=0xFF, rx_packet=None)
+```
+
+Examples:
+
+```py
+with_timeout = modem.recv(2000)
+
+print(repr(with_timeout))
+
+wait_forever = modem.recv()
+
+print(repr(wait_forever))
+```
+
+* `recv()` receives a LoRa packet from the modem.
+* Returns None on timeout, or an `RxPacket` instance with the packet on
+  success.
+* Optional arguments:
+   - `timeout_ms`. Optional, sets a receive timeout in milliseconds. If None
+   (default value), then the function will block indefinitely until a packet is
+   received.
+   - `rx_length`. Necessary to set if `implicit_header` is set to `True` (see
+     above). This is the length of the packet to receive. Ignored in the default
+     LoRa explicit header mode, where the received radio header includes the
+     length.
+   - `rx_packet`. Optional, this can be an `RxPacket` object previously
+     received from the modem. If the newly received packet has the same length,
+     this object is reused and returned to save an allocation. If the newly
+     received packet has a different length, a new `RxPacket` object is
+     allocated and returned instead.
+
+### RxPacket
+
+`RxPacket` is a class that wraps a `bytearray` holding the LoRa packet payload,
+meaning it can be passed anywhere that accepts a buffer object (like `bytes`,
+`bytearray`).
+
+However it also has the following metadata object variables:
+
+* `ticks_ms` - is a timestamp of `time.ticks_ms()` called at the time the
+    packet was received. Timestamp will be more accurate if the modem was
+    initialized to use interrupts.
+* `snr` - is the Signal to Noise ratio of the received packet, in units of `dB *
+    4`. Higher values indicate better signal.
+* `rssi` - is the Received Signal Strength indicator value in units of
+    dBm. Higher (less negative) values indicate more signal strength.
+* `crc_error` - In the default configuration, this value will always be False as
+  packets with invalid CRCs are dropped. If the `modem.rx_crc_error` flag is set
+  to True, then a packet with an invalid CRC will be returned with this flag set
+  to True.
+
+  Note that CRC is only ever checked on receive in particular configurations,
+  see the `crc_en` configuration item above for an explanation. If CRC is not
+  checked on receive, and `crc_error` will always be False.
+
+Example:
+
+```py
+rx = modem.recv(1000)
+
+if rx:
+    print(f'Received {len(rx)} byte packet at '
+          f'{rx.ticks_ms}ms, with SNR {rx.snr} '
+          f'RSSI {rx.rssi} valid_crc {rx.valid_crc}')
+```
+
+### Asynchronous API
+
+Not being able to do anything else while waiting for the modem is very
+limiting. Async Python is an excellent match for this kind of application!
+
+To use async Python, first install `lora-async` and then instantiate the async
+version of the LoRA modem class. The async versions have the prefix `Async` at
+the beginning of the class name. For example:
+
+```py
+import asyncio
+from lora import AsyncSX1276
+
+def get_async_modem():
+    # The LoRa configuration will depend on your board and location, see
+    # below under "Modem Configuration" for some possible examples.
+    lora_cfg = { 'freq_khz': SEE_BELOW_FOR_CORRECT_VALUE }
+
+    # To instantiate SPI correctly, see
+    # https://docs.micropython.org/en/latest/library/machine.SPI.html
+    spi = SPI(0, baudrate=2000_000)
+    cs = Pin(9)
+
+    # or AsyncSX1261, AsyncSX1262, AsyncSX1277, AsyncSX1278, SX1279, etc.
+    return AsyncSX1276(spi, cs,
+                       dio0=Pin(10),  # Optional, recommended
+                       dio1=Pin(11),  # Optional, recommended
+                       reset=Pin(13),  # Optional, recommended
+                       lora_cfg=lora_cfg)
+
+modem = get_async_modem()
+
+async def recv_coro():
+    rx = await modem.recv(2000)
+    if rx:
+        print(f'Received: {rx}')
+    else:
+        print('Timeout!')
+
+
+async def send_coro():
+    counter = 0
+    while True:
+        await modem.send(f'Hello world #{counter}'.encode())
+        print('Sent!')
+        await asyncio.sleep(5)
+        counter += 1
+
+async def init():
+    await asyncio.gather(
+        asyncio.create_task(send_coro()),
+        asyncio.create_task(recv_coro())
+    )
+
+asyncio.run(init())
+```
+
+For a more complete example, see `examples/reliable_delivery/sender_async.py`.
+
+* The `modem.recv()` and `modem.send()` coroutines take the same
+  arguments as the synchronous class' functions `recv()` and `send()`,
+  as documented above.
+* However, because these are async coroutines it's possible for other async
+  tasks to execute while they are blocked waiting for modem operations.
+* It is possible to await the `send()` coroutine while a `recv()`
+  is in progress. The receive will automatically resume once the modem finishes
+  sending. Send always has priority over receive.
+* However, at most one task should be awaiting each of receive and send. For
+  example, it's not possible for two tasks to `await modem.send()` at the
+  same time.
+
+#### Async Continuous Receive
+
+An additional API provides a Python async iterator that will continuously
+receive packets from the modem:
+
+```py
+async def keep_receiving():
+    async for packet in am.recv_continuous():
+        print(f'Received: {packet}')
+```
+
+For a more complete example, see `examples/reliable_delivery/receiver_async.py`.
+
+Receiving will continue and the iterator will yield packets unless another task
+calls `modem.stop()` or `modem.standby()` (see below for a description of these
+functions).
+
+Same as the async `recv()` API, it's possible for another task to send while
+this iterator is in use.
+
+## Low-Level API
+
+This API allows other code to execute while waiting for LoRa operations, without
+using asyncio coroutines.
+
+This is a traditional asynchronous-style API that requires manual management of
+modem timing, interrupts, packet timeouts, etc. It's very easy to write
+spaghetti code with this API. If asyncio is available on your board, the async
+Python API is probably an easier choice to get the same functionality with less
+complicated code.
+
+However, if you absolutely need maximum control over the modem and the rest of
+your board then this may be the API for you!
+
+### Receiving
+
+```py
+will_irq =  modem.start_recv(timeout_ms=1000, continuous=False)
+
+rx = True
+while rx is True:
+    if will_irq:
+        # Add code to sleep and wait for an IRQ,
+        # if necessary call modem.irq_triggered() to verify
+        # that the modem IRQ was actually triggered.
+        pass
+    rx = modem.poll_recv()
+
+    # Do anything else you need the application to do
+
+if rx:  # isinstance(rx, lora.RxPacket)
+    print(f'Received: {rx}')
+else:  # rx is False
+    print('Timed out')
+```
+
+For an example that uses the low-level receive API for continuous receive, see
+`examples/reliable_delivery/receiver.py`.
+
+The steps to receive packet(s) with the low-level API are:
+
+1. Call `modem.start_recv(timeout_ms=None, continuous=False, rx_length=0xFF)`.
+
+   - `timeout_ms` is an optional timeout in milliseconds, same as the Simple API
+     recv().
+   - Set `continuous=True` for the modem to continuously receive and not go into
+     standby after the first packet is received. If setting `continuous` to
+     `True`, `timeout_ms` must be `None`.
+   - `rx_length` is an optional argument, only used when LoRa implicit headers
+     are configured.  See the Simple API description above for details.
+
+   The return value of this function is truthy if interrupts will be used for
+   the receive, falsey otherwise.
+2. If interrupts are being used, wait for an interrupt to occur. Steps may include
+   configuring the modem interrupt pins as wake sources and putting the host
+   into a light sleep mode. See the general description of "Interrupts", below.
+
+   Alternatively, if `timeout_ms` was set then caller can wait for at least the
+   timeout period before checking if the modem received anything or timed out.
+
+    It is also possible to simply call `poll_recv()` in a loop, but doing
+    this too frequently may significantly degrade the RF receive performance
+    depending on the hardware.
+
+3. Call `modem.poll_recv()`. This function checks the receive state and
+   returns a value indicating the current state:
+
+   - `True` if the modem is still receiving and the caller should call this
+     function again in the future. This can be caused by any of:
+
+     * Modem is still waiting in 'single' mode (`continuous=False`) to receive a
+       packet or time out.
+     * Modem is in continuous receive mode so will always be receiving.
+     * The modem is actually sending right now, but the driver will resume
+       receiving after the send completes.
+     * The modem received a packet with an invalid CRC (and `modem.rx_crc_error
+       = False`). The driver has just now discarded it and resumed the modem
+       receive operation.
+
+   - `False` if the modem is not currently receiving. This can be caused by any
+     of:
+
+     * No receive has been started.
+     * A single receive has timed out.
+     * The receive was aborted. See the `standby()` and `sleep()` functions
+       below.
+
+   - An instance of the `RxPacket` class. This means the modem has received this
+     packet since the last call to `poll_recv()`. Whether or not the modem is
+     still receiving after this depends on whether the receive was started in
+     `continuous` mode or not.)
+
+4. If `poll_recv()` returned `True`, go back to step 2 and wait for the next
+   opportunity to call `poll_recv()`. (Note that it's necessary to test using
+   `is True` to distinguish between `True` and a new packet.)
+
+It is possible to also send packets while receiving and looping between
+steps 2 and 4. The driver will automatically suspend receiving and resume it
+again once sending is done. It's OK to call either the Simple API
+`send()` function or the low-level send API (see below) in order to do
+this.
+
+The purpose of the low-level API is to allow code to perform other unrelated
+functions during steps 2 and 3. It's still recommended to call
+`modem.poll_recv()` as soon as possible after a modem interrupt has
+occurred, especially in continuous receive mode when multiple packets may be
+received rapidly.
+
+To cancel a receive in progress, call `modem.standby()` or `modem.sleep()`, see
+below for descriptions of these functions.
+
+*Important*: None of the MicroPython lora driver is thread-safe. It's OK for
+different MicroPython threads to manage send and receive, but the caller is
+responsible for adding locking so that different threads are not calling any
+modem APIs concurrently. Async MicroPython may provide a cleaner and simpler
+choice for this kind of firmware architecture.
+
+### Sending
+
+The low-level API for sending is similar to the low-level API for receiving:
+
+1. Call `modem.prepare_send(payload)` with the packet payload. This will put
+   the modem into standby (pausing receive if necessary), configure the modem
+   registers, and copy the payload into the modem FIFO buffer.
+2. Call `modem.start_send(packet)` to actually start sending.
+
+   Sending is split into these two steps to allow accurate send
+   timing. `prepare_send()` may take a variable amount of time to copy data
+   to the modem, configure registers, etc. Then `start_send()` only performs
+   the minimum fixed duration operation to start sending, so transmit
+   should start very soon after this function is called.
+
+   The return value of `start_send()` function is truthy if an interrupt is
+   enabled to signal the send completing, falsey otherwise.
+
+   Not calling both `prepare_send()` or `start_send()` in order, or
+   calling any other modem functions between `prepare_send()` and
+   `start_send()`, is not supported and will result in incorrect behaviour.
+
+3. Wait for the send to complete. This is possible in any of three
+   different ways:
+   -  If interrupts are enabled, wait for an interrupt to occur. Steps may include
+      configuring the modem interrupt pins as wake sources and putting the host
+      into a light sleep mode. See the general description of "Interrupts", below.
+   - Calculate the packet "time on air" by calling
+     `modem.get_time_on_air_us(len(packet))` and wait at least this long.
+   - Call `modem.poll_send()` in a loop (see next step) until it confirms
+     the send has completed.
+4. Call `modem.poll_send()` to check transmission state, and to
+   automatically resume a receive operation if one was suspended by
+   `prepare_send()`. The result of this function is one of:
+
+    - `True` if a send is in progress and the caller
+      should call again.
+
+    - `False` if no send is in progress.
+
+    - An `int` value. This is returned the first time `poll_send()` is
+      called after a send ended. The value is the `time.ticks_ms()`
+      timestamp of the time that the send completed. If interrupts are
+      enabled, this is the time the "send done" ISR executed. Otherwise, it
+      will be the time that `poll_send()` was just called.
+
+   Note that `modem.poll_send()` returns an `int` only one time per
+   successful transmission. Any subsequent calls will return `False` as there is
+   no longer a send in progress.
+
+   To abort a send in progress, call `modem.standby()` or `modem.sleep()`,
+   see the descriptions of these functions below. Subsequent calls to
+   `poll_send()` will return `False`.
+5. If `poll_send()` returned `True`, repeat steps 3 through 5.
+
+*Important*: Unless a transmission is aborted, `poll_send()` **MUST be
+called** at least once after `start_send()` and should be repeatedly called
+until it returns a value other than `True`. `poll_send()` can also be called
+after a send is aborted, but this is optional. If `poll_send()` is not
+called correctly then the driver's internal state will not correctly update and
+no subsequent receive will be able to start.
+
+It's also possible to mix the simple `send()` API with the low-level receive
+API, if this is more convenient for your application.
+
+### Interrupts
+
+If interrupt pins are in use then it's important for a programmer using the
+low-level API to handle interrupts correctly.
+
+It's only possible to rely on interrupts if the correct hardware interrupt lines
+are configured. Consult the modem reference datasheet, or check if the value of
+`start_recv()` or `start_send()` is truthy, in order to know if hardware
+interrupts can be used. Otherwise, the modem must be polled to know when an
+operation has completed.
+
+There are two kinds of interrupts:
+
+* A hardware interrupt (set in the driver by `Pin.irq()`) will be triggered on
+  the rising edge of a modem interrupt line (DIO0, DIO1, etc). The driver will
+  attempt to configure these for `RX Done`, `RX Timeout` and `TX Done` events if
+  possible and applicable for the modem operation, and will handle them.
+
+  It's possible for the programmer to configure these pins as hardware wake sources
+  and put the board into a low-power sleep mode, to be woken when the modem
+  finishes its operation.
+* A "soft" interrupt is triggered by the driver if an operation is aborted (see
+  `standby()` and `sleep()`, below), or if a receive operation "soft times
+  out". A receive "soft times out" if a receive is paused by a send
+  operation and after the send operation completes then the timeout period
+  for the receive has already elapsed. In these cases, the driver's radio ISR
+  routine is called but no hardware interrupt occurs.
+
+To detect if a modem interrupt has occurred, the programmer can use any of the
+following different approaches:
+
+* Port-specific functions to determine a hardware wakeup cause. Note that this
+  can only detect hardware interrupts.
+* Call the `modem.irq_triggered()` function. This is a lightweight function that
+  returns True if the modem ISR has been executed since the last time a send
+  or receive started. It is cleared when `poll_recv()` or `poll_send()`
+  is called after an interrupt, or when a new operation is started. The idea is
+  to use this as a lightweight "should I call `poll_recv()` or
+  `poll_send()` now?" check function if there's no easy way to determine
+  which interrupt has woken the board up.
+* Implement a custom interrupt callback function and call
+  `modem.set_irq_callback()` to install it. The function will be called if a
+  hardware interrupt occurs, possibly in hard interrupt context. Refer to the
+  documentation about [writing interrupt handlers][isr_rules] for more
+  information. It may also be called if the driver triggers a soft interrupt.
+  The `lora-async` modem classes install their own callback here, so it's not
+  possible to mix this approach with the provided asynchronous API.
+* Call `modem.poll_recv()` or `modem.poll_send()`. This takes more time
+  and uses more power as it reads the modem IRQ status directly from the modem
+  via SPI, but it also give the most definite result.
+
+As a "belts and braces" protection against unknown driver bugs or modem bugs,
+it's best practice to not rely on an interrupt occurring and to also include
+some logic that periodically times out and polls the modem state "just in case".
+
+## Other Functions
+
+### CRC Error Counter
+
+Modem objects have a variable `modem.crc_errors` which starts at `0` and
+is incremented by one each time a received CRC error or packet header error is
+detected by the modem. The programmer can read this value to know the current CRC
+error count, and also write it (for example, to clear it periodically by setting
+to `0`).
+
+For an alternative method to know about CRC errors when they occur, set
+`modem.rx_crc_error = True` (see `crc_en`, above, for more details.)
+
+### Modem Standby
+
+Calling `modem.standby()` puts the modem immediately into standby mode. In the
+case of SX1261 and SX1262, the 32MHz oscillator is started.
+
+Any current send or receive operations are immediately aborted.  The
+implications of this depends on the API in use:
+
+* The simple API does not support calling `standby()` while a receive or
+  send is in progress.
+* The async API handles this situation automatically. Any blocked `send()`
+  or `recv()` async coroutine will return None. The `recv_continuous()`
+  iterator will stop iterating.
+* The low-level API relies on the programmer to handle this case. When the modem
+  goes to standby, a "soft interrupt" occurs that will trigger the radio ISR and
+  any related callback, but this is not a hardware interrupt so may not wake the
+  CPU if the programmer has put it back to sleep. Any subsequent calls to
+  `poll_recv()` or `poll_send()` will both return `(False, None)` as no
+  operation is in progress. The programmer needs to ensure that any code that is
+  blocking waiting for an interrupt has the chance to wake up and call
+  `poll_recv()` and/or `poll_send()` to detect that the operation(s) have
+  been aborted.
+
+### Modem Sleep
+
+Calling `modem.sleep()` puts the modem into a low power sleep mode with
+configuration retention. The modem will automatically wake the next time an
+operation is started, or can be woken manually by calling
+`modem.standby()`. Waking the modem may take some time, consult the modem
+datasheet for details.
+
+As with `standby()`, any current send or receive operations are immediately
+aborted. The implications of this are the same as listed for standby, above.
+
+### Check if modem is idle
+
+The `modem.is_idle()` function will return True unless the modem is currently
+sending or receiving.
+
+### Packet length calculations
+
+Calling `modem.get_time_on_air_us(plen)` will return the "on air time" in
+microseconds for a packet of length `plen`, according to the current modem
+configuration. This can be used to synchronise modem operations, choose
+timeouts, or predict when a send will complete.
+
+Unlike the other modem API functions, this function doesn't interact with
+hardware at all so it can be safely called concurrently with other modem APIs.
+
+## Antenna switch object
+
+The modem constructors have an optional `ant_sw` parameter which allows passing
+in an antenna switch object to be called by the driver. This allows
+automatically configuring some GPIOs or other hardware settings each time the
+modem changes between TX and RX modes, and goes idle.
+
+The argument should be an object which implements three functions: `tx(tx_arg)`,
+`rx()`, and `idle()`. For example:
+
+```py
+class MyAntennaSwitch:
+    def tx(self, tx_arg):
+        ant_sw_gpio(1)  # Set GPIO high
+
+    def rx(self):
+        ant_sw_gpio(0)  # Set GPIO low
+
+    def idle(self):
+        pass
+```
+
+* `tx()` is called a short time before the modem starts sending.
+* `rx()` is called a short time before the modem starts receiving.
+* `idle()` is called at some point after each send or receive completes, and
+  may be called multiple times.
+
+The meaning of `tx_arg` depends on the modem:
+
+* For SX127x it is `True` if the `PA_BOOST` `tx_ant` setting is in use (see
+  above), and `False` otherwise.
+* For SX1262 it is `True` (indicating High Power mode).
+* For SX1261 it is `False` (indicating Low Power mode).
+* For WL55SubGhzModem it is `True` if the `PA_BOOST` `tx_ant` setting is in use (see above), and `False` otherwise.
+
+This parameter can be ignored if it's already known what modem and antenna is being used.
+
+### WL55SubGhzModem ant_sw
+
+When instantiating the `WL55SubGhzModem` and `AsyncWL55SubGHzModem` classes, the
+default `ant_sw` parameter is not `None`. Instead, the default will instantiate
+an object of type `lora.NucleoWL55RFConfig`. This implements the antenna switch
+connections for the ST NUCLEO-WL55 development board (as connected to GPIO pins
+C4, C5 and C3). See ST document [UM2592][ST-UM2592-p27] (PDF) Figure 18 for details.
+
+When using these modem classes (only), to disable any automatic antenna
+switching behaviour it's necessary to explicitly set `ant_sw=None`.
+
+## Troubleshooting
+
+Some common errors and their causes:
+
+### RuntimeError: BUSY timeout
+
+The SX1261/2 drivers will raise this exception if the modem's TCXO fails to
+provide the necessary clock signal when starting a transmit or receive
+operation, or moving into "standby" mode.
+
+Sometimes, this means the constructor parameter `dio3_tcxo_millivolts` (see above)
+must be set as the SX126x chip DIO3 output pin is the power source for the TCXO
+connected to the modem.  Often this parameter should be set to `3300` (3.3V) but
+it may be another value, consult the documentation for your LoRa modem module.
+
+[isr_rules]: https://docs.micropython.org/en/latest/reference/isr_rules.html
+[ST-UM2592-p27]: https://www.st.com/resource/en/user_manual/dm00622917-stm32wl-nucleo64-board-mb1389-stmicroelectronics.pdf#page=27
diff --git a/micropython/lora/examples/reliable_delivery/README.md b/micropython/lora/examples/reliable_delivery/README.md
new file mode 100644
index 000000000..878b0f8a5
--- /dev/null
+++ b/micropython/lora/examples/reliable_delivery/README.md
@@ -0,0 +1,93 @@
+# LoRa Reliable Delivery Example
+
+This example shows a basic custom protocol for reliable one way communication
+from low-power remote devices to a central base device:
+
+- A single "receiver" device, running on mains power, listens continuously for
+  messages from one or more "sender" devices. Messages are payloads inside LoRa packets,
+  with some additional framing and address in the LoRa packet payload.
+- "Sender" devices are remote sensor nodes, possibly battery powered. These wake
+  up periodically, read some data from a sensor, and send it in a message to the receiver.
+- Messages are transmitted "reliably" with some custom header information,
+  meaning the receiver will acknowledge it received each message and the sender
+  will retry sending if it doesn't receive the acknowledgement.
+
+## Source Files
+
+* `lora_rd_settings.py` contains some common settings that are imported by
+  sender and receiver. These settings will need to be modified for the correct
+  frequency and other settings, before running the examples.
+* `receiver.py` and `receiver_async.py` contain a synchronous (low-level API)
+  and asynchronous (iterator API) implementation of the same receiver program,
+  respectively. These two programs should work the same, they are intended show
+  different ways the driver can be used.
+* `sender.py` and `sender_async.py` contain a synchronous (simple API) and
+  asynchronous (async API) implementation of the same sender program,
+  respectively. Because the standard async API resembles the Simple API, these
+  implementations are *very* similar. The two programs should work the same,
+  they are intended to show different ways the driver can be used.
+
+## Running the examples
+
+One way to run this example interactively:
+
+1. Install or "freeze in" the necessary lora modem driver package (`lora-sx127x`
+   or `lora-sx126x`) and optionally the `lora-async` package if using the async
+   examples (see main lora `README.md` in the above directory for details).
+2. Edit the `lora_rd_settings.py` file to set the frequency and other protocol
+   settings for your region and hardware (see main lora `README.md`).
+3. Edit the program you plan to run and fill in the `get_modem()` function with
+   the correct modem type, pin assignments, etc. for your board (see top-level
+   README). Note the `get_modem()` function should use the existing `lora_cfg`
+   variable, which holds the settings imported from `lora_rd_settings.py`.
+4. Change to this directory in a terminal.
+5. Run `mpremote mount . exec receiver.py` on one board and `mpremote mount
+   . exec sender.py` on another (or swap in `receiver_async.py` and/or
+   `sender_async.py` as desired).
+
+Consult the [mpremote
+documentation](https://docs.micropython.org/en/latest/reference/mpremote.html)
+for an explanation of these commands and the options needed to run two copies of
+`mpremote` on different serial ports at the same time.
+
+## Automatic Performance Tuning
+
+- When sending an ACK, the receiver includes the RSSI of the received
+  packet. Senders will automatically modify their output_power to minimize the
+  power consumption required to reach the receiver. Similarly, if no ACK is
+  received then they will increase their output power and also re-run Image
+  calibration in order to maximize RX performance.
+
+## Message payloads
+
+Messages are LoRa packets, set up as follows:
+
+LoRA implicit header mode, CRCs enabled.
+
+* Each remote device has a unique sixteen-bit ID (range 00x0000 to 0xFFFE). ID
+  0xFFFF is reserved for the single receiver device.
+* An eight-bit message counter is used to identify duplicate messages
+
+* Data message format is:
+  - Sender ID (two bytes, little endian)
+  - Counter byte (incremented on each new message, not incremented on retry).
+  - Message length (1 byte)
+  - Message (variable length)
+  - Checksum byte (sum of all proceeding bytes in message, modulo 256). The LoRa
+    packet has its own 16-bit CRC, this is included as an additional way to
+    disambiguate other LoRa packets that might appear the same.
+
+* After receiving a valid data message, the receiver device should send
+  an acknowledgement message 25ms after the modem receive completed.
+
+  Acknowledgement message format:
+  - 0xFFFF (receiver station ID as two bytes)
+  - Sender's Device ID from received message (two bytes, little endian)
+  - Counter byte from received message
+  - Checksum byte from received message
+  - RSSI value as received by radio (one signed byte)
+
+* If the remote device doesn't receive a packet with the acknowledgement
+  message, it retries up to a configurable number of times (default 4) with a
+  basic exponential backoff formula.
+
diff --git a/micropython/lora/examples/reliable_delivery/lora_rd_settings.py b/micropython/lora/examples/reliable_delivery/lora_rd_settings.py
new file mode 100644
index 000000000..bbf03da5d
--- /dev/null
+++ b/micropython/lora/examples/reliable_delivery/lora_rd_settings.py
@@ -0,0 +1,38 @@
+# MicroPython lora reliable_delivery example - common protocol settings
+# MIT license; Copyright (c) 2023 Angus Gratton
+
+#
+######
+# To be able to be able to communicate, most of these settings need to match on both radios.
+# Consult the example README for more information about how to use the example.
+######
+
+# LoRa protocol configuration
+#
+# Currently configured for relatively slow & low bandwidth settings, which
+# gives more link budget and possible range.
+#
+# These settings should match on receiver.
+#
+# Check the README and local regulations to know what configuration settings
+# are available.
+lora_cfg = {
+    "freq_khz": 916000,
+    "sf": 10,
+    "bw": "62.5",  # kHz
+    "coding_rate": 8,
+    "preamble_len": 12,
+    "output_power": 10,  # dBm
+}
+
+# Single receiver has a fixed 16-bit ID value (senders each have a unique value).
+RECEIVER_ID = 0xFFFF
+
+# Length of an ACK message in bytes.
+ACK_LENGTH = 7
+
+# Send the ACK this many milliseconds after receiving a valid message
+#
+# This can be quite a bit lower (25ms or so) if wakeup times are short
+# and _DEBUG is turned off on the modems (logging to UART delays everything).
+ACK_DELAY_MS = 100
diff --git a/micropython/lora/examples/reliable_delivery/receiver.py b/micropython/lora/examples/reliable_delivery/receiver.py
new file mode 100644
index 000000000..2ab4231db
--- /dev/null
+++ b/micropython/lora/examples/reliable_delivery/receiver.py
@@ -0,0 +1,163 @@
+# MicroPython lora reliable_delivery example - synchronous receiver program
+# MIT license; Copyright (c) 2023 Angus Gratton
+import struct
+import time
+import machine
+from machine import SPI, Pin
+from micropython import const
+from lora import RxPacket
+
+from lora_rd_settings import RECEIVER_ID, ACK_LENGTH, ACK_DELAY_MS, lora_cfg
+
+# Change _DEBUG to const(True) to get some additional debugging output
+# about timing, RSSI, etc.
+#
+# For a lot more debugging detail, go to the modem driver and set _DEBUG there to const(True)
+_DEBUG = const(False)
+
+# Keep track of the last counter value we got from each known sender
+# this allows us to tell if packets are being lost
+last_counters = {}
+
+
+def get_modem():
+    # from lora import SX1276
+    # return SX1276(
+    #     spi=SPI(1, baudrate=2000_000, polarity=0, phase=0,
+    #             miso=Pin(19), mosi=Pin(27), sck=Pin(5)),
+    #     cs=Pin(18),
+    #     dio0=Pin(26),
+    #     dio1=Pin(35),
+    #     reset=Pin(14),
+    #     lora_cfg=lora_cfg,
+    # )
+    raise NotImplementedError("Replace this function with one that returns a lora modem instance")
+
+
+def main():
+    print("Initializing...")
+    modem = get_modem()
+
+    print("Main loop started")
+    receiver = Receiver(modem)
+
+    while True:
+        # With wait=True, this function blocks until something is received and always
+        # returns non-None
+        sender_id, data = receiver.recv(wait=True)
+
+        # Do something with the data!
+        print(f"Received {data} from {sender_id:#x}")
+
+
+class Receiver:
+    def __init__(self, modem):
+        self.modem = modem
+        self.last_counters = {}  # Track the last counter value we got from each sender ID
+        self.rx_packet = None  # Reuse RxPacket object when possible, save allocation
+        self.ack_buffer = bytearray(ACK_LENGTH)  # reuse the same buffer for ACK packets
+        self.skipped_packets = 0  # Counter of skipped packets
+
+        modem.calibrate()
+
+        # Start receiving immediately. We expect the modem to receive continuously
+        self.will_irq = modem.start_recv(continuous=True)
+        print("Modem initialized and started receive...")
+
+    def recv(self, wait=True):
+        # Receive a packet from the sender, including sending an ACK.
+        #
+        # Returns a tuple of the 16-bit sender id and the sensor data payload.
+        #
+        # This function should be called very frequently from the main loop (at
+        # least every ACK_DELAY_MS milliseconds), to avoid not sending ACKs in time.
+        #
+        # If 'wait' argument is True (default), the function blocks indefinitely
+        # until a packet is received. If False then it will return None
+        # if no packet is available.
+        #
+        # Note that because we called start_recv(continuous=True), the modem
+        # will keep receiving on its own - even if when we call send() to
+        # send an ACK.
+        while True:
+            rx = self.modem.poll_recv(rx_packet=self.rx_packet)
+
+            if isinstance(rx, RxPacket):  # value will be True or an RxPacket instance
+                decoded = self._handle_rx(rx)
+                if decoded:
+                    return decoded  # valid LoRa packet and valid for this application
+
+            if not wait:
+                return None
+
+            # Otherwise, wait for an IRQ (or have a short sleep) and then poll recv again
+            # (receiver is not a low power node, so don't bother with sleep modes.)
+            if self.will_irq:
+                while not self.modem.irq_triggered():
+                    machine.idle()
+            else:
+                time.sleep_ms(1)
+
+    def _handle_rx(self, rx):
+        # Internal function to handle a received packet and either send an ACK
+        # and return the sender and the payload, or return None if packet
+        # payload is invalid or a duplicate.
+
+        if len(rx) < 5:  # 4 byte header plus 1 byte checksum
+            print("Invalid packet length")
+            return None
+
+        sender_id, counter, data_len = struct.unpack("<HBB", rx)
+        csum = rx[-1]
+
+        if len(rx) != data_len + 5:
+            print("Invalid length in payload header")
+            return None
+
+        calc_csum = sum(b for b in rx[:-1]) & 0xFF
+        if csum != calc_csum:
+            print(f"Invalid checksum. calc={calc_csum:#x} received={csum:#x}")
+            return None
+
+        # Packet is valid!
+
+        if _DEBUG:
+            print(f"RX {data_len} byte message RSSI {rx.rssi} at timestamp {rx.ticks_ms}")
+
+        # Send the ACK
+        struct.pack_into(
+            "<HHBBb", self.ack_buffer, 0, RECEIVER_ID, sender_id, counter, csum, rx.rssi
+        )
+
+        # Time send to start as close to ACK_DELAY_MS after message was received as possible
+        tx_at_ms = time.ticks_add(rx.ticks_ms, ACK_DELAY_MS)
+        tx_done = self.modem.send(self.ack_buffer, tx_at_ms=tx_at_ms)
+
+        if _DEBUG:
+            tx_time = time.ticks_diff(tx_done, tx_at_ms)
+            expected = self.modem.get_time_on_air_us(ACK_LENGTH) / 1000
+            print(f"ACK TX {tx_at_ms}ms -> {tx_done}ms took {tx_time}ms expected {expected}")
+
+        # Check if the data we received is fresh or stale
+        if sender_id not in self.last_counters:
+            print(f"New device id {sender_id:#x}")
+        elif self.last_counters[sender_id] == counter:
+            print(f"Duplicate packet received from {sender_id:#x}")
+            return None
+        elif counter != 1:
+            # If the counter from this sender has gone up by more than 1 since
+            # last time we got a packet, we know there is some packet loss.
+            #
+            # (ignore the case where the new counter is 1, as this probably
+            # means a reset.)
+            delta = (counter - 1 - self.last_counters[sender_id]) & 0xFF
+            if delta:
+                print(f"Skipped/lost {delta} packets from {sender_id:#x}")
+                self.skipped_packets += delta
+
+        self.last_counters[sender_id] = counter
+        return sender_id, rx[4:-1]
+
+
+if __name__ == "__main__":
+    main()
diff --git a/micropython/lora/examples/reliable_delivery/receiver_async.py b/micropython/lora/examples/reliable_delivery/receiver_async.py
new file mode 100644
index 000000000..72a456db8
--- /dev/null
+++ b/micropython/lora/examples/reliable_delivery/receiver_async.py
@@ -0,0 +1,121 @@
+# MicroPython lora reliable_delivery example - asynchronous receiver program
+# MIT license; Copyright (c) 2023 Angus Gratton
+import struct
+import time
+import asyncio
+from machine import SPI, Pin
+from micropython import const
+
+from lora_rd_settings import RECEIVER_ID, ACK_LENGTH, ACK_DELAY_MS, lora_cfg
+
+# Change _DEBUG to const(True) to get some additional debugging output
+# about timing, RSSI, etc.
+#
+# For a lot more debugging detail, go to the modem driver and set _DEBUG there to const(True)
+_DEBUG = const(False)
+
+# Keep track of the last counter value we got from each known sender
+# this allows us to tell if packets are being lost
+last_counters = {}
+
+
+def get_async_modem():
+    # from lora import AsyncSX1276
+    # return AsyncSX1276(
+    #     spi=SPI(1, baudrate=2000_000, polarity=0, phase=0,
+    #             miso=Pin(19), mosi=Pin(27), sck=Pin(5)),
+    #     cs=Pin(18),
+    #     dio0=Pin(26),
+    #     dio1=Pin(35),
+    #     reset=Pin(14),
+    #     lora_cfg=lora_cfg,
+    # )
+    raise NotImplementedError("Replace this function with one that returns a lora modem instance")
+
+
+def main():
+    # Initializing the modem.
+    #
+
+    print("Initializing...")
+    modem = get_async_modem()
+    asyncio.run(recv_continuous(modem, rx_callback))
+
+
+async def rx_callback(sender_id, data):
+    # Do something with the data!
+    print(f"Received {data} from {sender_id:#x}")
+
+
+async def recv_continuous(modem, callback):
+    # Async task which receives packets from the AsyncModem recv_continuous()
+    # iterator, checks if they are valid, and send back an ACK if needed.
+    #
+    # On each successful message, we await callback() to allow the application
+    # to do something with the data. Callback args are sender_id (as int) and the bytes
+    # of the message payload.
+
+    last_counters = {}  # Track the last counter value we got from each sender ID
+    ack_buffer = bytearray(ACK_LENGTH)  # reuse the same buffer for ACK packets
+    skipped_packets = 0  # Counter of skipped packets
+
+    modem.calibrate()
+
+    async for rx in modem.recv_continuous():
+        # Filter 'rx' packet to determine if it's valid for our application
+        if len(rx) < 5:  # 4 byte header plus 1 byte checksum
+            print("Invalid packet length")
+            continue
+
+        sender_id, counter, data_len = struct.unpack("<HBB", rx)
+        csum = rx[-1]
+
+        if len(rx) != data_len + 5:
+            print("Invalid length in payload header")
+            continue
+
+        calc_csum = sum(b for b in rx[:-1]) & 0xFF
+        if csum != calc_csum:
+            print(f"Invalid checksum. calc={calc_csum:#x} received={csum:#x}")
+            continue
+
+        # Packet is valid!
+
+        if _DEBUG:
+            print(f"RX {data_len} byte message RSSI {rx.rssi} at timestamp {rx.ticks_ms}")
+
+        # Send the ACK
+        struct.pack_into("<HHBBb", ack_buffer, 0, RECEIVER_ID, sender_id, counter, csum, rx.rssi)
+
+        # Time send to start as close to ACK_DELAY_MS after message was received as possible
+        tx_at_ms = time.ticks_add(rx.ticks_ms, ACK_DELAY_MS)
+        tx_done = await modem.send(ack_buffer, tx_at_ms=tx_at_ms)
+
+        if _DEBUG:
+            tx_time = time.ticks_diff(tx_done, tx_at_ms)
+            expected = modem.get_time_on_air_us(ACK_LENGTH) / 1000
+            print(f"ACK TX {tx_at_ms}ms -> {tx_done}ms took {tx_time}ms expected {expected}")
+
+        # Check if the data we received is fresh or stale
+        if sender_id not in last_counters:
+            print(f"New device id {sender_id:#x}")
+        elif last_counters[sender_id] == counter:
+            print(f"Duplicate packet received from {sender_id:#x}")
+            continue
+        elif counter != 1:
+            # If the counter from this sender has gone up by more than 1 since
+            # last time we got a packet, we know there is some packet loss.
+            #
+            # (ignore the case where the new counter is 1, as this probably
+            # means a reset.)
+            delta = (counter - 1 - last_counters[sender_id]) & 0xFF
+            if delta:
+                print(f"Skipped/lost {delta} packets from {sender_id:#x}")
+                skipped_packets += delta
+
+        last_counters[sender_id] = counter
+        await callback(sender_id, rx[4:-1])
+
+
+if __name__ == "__main__":
+    main()
diff --git a/micropython/lora/examples/reliable_delivery/sender.py b/micropython/lora/examples/reliable_delivery/sender.py
new file mode 100644
index 000000000..957e9d824
--- /dev/null
+++ b/micropython/lora/examples/reliable_delivery/sender.py
@@ -0,0 +1,213 @@
+# MicroPython lora reliable_delivery example - synchronous sender program
+# MIT license; Copyright (c) 2023 Angus Gratton
+import machine
+from machine import SPI, Pin
+import random
+import struct
+import time
+
+from lora_rd_settings import RECEIVER_ID, ACK_LENGTH, ACK_DELAY_MS, lora_cfg
+
+SLEEP_BETWEEN_MS = 5000  # Main loop should sleep this long between sending data to the receiver
+
+MAX_RETRIES = 4  # Retry each message this often if no ACK is received
+
+# Initial retry is after this long. Increases by 1.25x each subsequent retry.
+BASE_RETRY_TIMEOUT_MS = 1000
+
+# Add random jitter to each retry period, up to this long. Useful to prevent two
+# devices ending up in sync.
+RETRY_JITTER_MS = 1500
+
+# If reported RSSI value is lower than this, increase
+# output power 1dBm
+RSSI_WEAK_THRESH = -110
+
+# If reported RSSI value is higher than this, decrease
+# output power 1dBm
+RSSI_STRONG_THRESH = -70
+
+# IMPORTANT: Set this to the maximum output power in dBm that is permitted in
+# your regulatory environment.
+OUTPUT_MAX_DBM = 15
+OUTPUT_MIN_DBM = -20
+
+
+def get_modem():
+    # from lora import SX1276
+    # return SX1276(
+    #     spi=SPI(1, baudrate=2000_000, polarity=0, phase=0,
+    #             miso=Pin(19), mosi=Pin(27), sck=Pin(5)),
+    #     cs=Pin(18),
+    #     dio0=Pin(26),
+    #     dio1=Pin(35),
+    #     reset=Pin(14),
+    #     lora_cfg=lora_cfg,
+    # )
+    raise NotImplementedError("Replace this function with one that returns a lora modem instance")
+
+
+def main():
+    modem = get_modem()
+
+    # Unique ID of this sender, 16-bit number. This method of generating an ID is pretty crummy,
+    # if using this in a real application then probably better to store these in the filesystem or
+    # something like that
+    DEVICE_ID = sum(b for b in machine.unique_id()) & 0xFFFF
+
+    sender = Sender(modem, DEVICE_ID)
+    while True:
+        sensor_data = get_sensor_data()
+        sender.send(sensor_data)
+
+        # Sleep until the next time we should read the sensor data and send it to
+        # the receiver.
+        #
+        # The goal for the device firmware is to spend most of its time in the lowest
+        # available sleep state, to save power.
+        #
+        # Note that if the sensor(s) in a real program generates events, these can be
+        # hooked to interrupts and used to wake Micropython up to send data,
+        # instead.
+        modem.sleep()
+        time.sleep_ms(SLEEP_BETWEEN_MS)  # TODO see if this can be machine.lightsleep()
+
+
+def get_sensor_data():
+    # Return a bytes object with the latest sensor data to send to the receiver.
+    #
+    # As this is just an example, we send a dummy payload which is just a string
+    # containing our ticks_ms() timestamp.
+    #
+    # In a real application the sensor data should usually be binary data and
+    # not a string, to save transmission size.
+    return f"Hello, ticks_ms={time.ticks_ms()}".encode()
+
+
+class Sender:
+    def __init__(self, modem, device_id):
+        self.modem = modem
+        self.device_id = device_id
+        self.counter = 0
+        self.output_power = lora_cfg["output_power"]  # start with common settings power level
+        self.rx_ack = None  # reuse the ack message object when we can
+
+        print(f"Sender initialized with ID {device_id:#x}")
+        random.seed(device_id)
+        self.adjust_output_power(0)  # set the initial value within MIN/MAX
+
+        modem.calibrate()
+
+    def send(self, sensor_data, adjust_output_power=True):
+        # Send a packet of sensor data to the receiver reliably.
+        #
+        # Returns True if data was successfully sent and ACKed, False otherwise.
+        #
+        # If adjust_output_power==True then increase or decrease output power
+        # according to the RSSI reported in the ACK packet.
+        self.counter = (self.counter + 1) & 0xFF
+
+        # Prepare the simple payload with header and checksum
+        # See README for a summary of the simple data message format
+        payload = bytearray(len(sensor_data) + 5)
+        struct.pack_into("<HBB", payload, 0, self.device_id, self.counter, len(sensor_data))
+        payload[4:-1] = sensor_data
+        payload[-1] = sum(b for b in payload) & 0xFF
+
+        # Calculate the time on air (in milliseconds) for an ACK packet
+        ack_packet_ms = self.modem.get_time_on_air_us(ACK_LENGTH) // 1000 + 1
+        timeout = BASE_RETRY_TIMEOUT_MS
+
+        print(f"Sending {len(payload)} bytes")
+
+        # Send the payload, until we receive an acknowledgement or run out of retries
+        for _ in range(MAX_RETRIES):
+            # Using simple API here.
+            #
+            # We could reduce power consumption a little by going to sleep
+            # instead of waiting for the send/recv to complete, but doing
+            # this well involves setting port-specific wakeup settings. Provided
+            # "dio" pin is assigned then the MCU will mostly be in a lower power
+            # idle state while the radio sends, at least.
+            sent_at = self.modem.send(payload)
+
+            # We expect the receiver of a valid message to start sending the ACK
+            # approximately ACK_DELAY_MS after receiving the message (to allow
+            # the sender time to reconfigure the modem.)
+            #
+            # We start receiving as soon as we can, but allow up to
+            # ACK_DELAY_MS*2 of total timing leeway - plus the time on air for
+            # the message itself
+            maybe_ack = self.modem.recv(ack_packet_ms + ACK_DELAY_MS * 2, rx_packet=self.rx_ack)
+
+            # Check if the packet we received is a valid ACK
+            rssi = self._ack_is_valid(maybe_ack, payload[-1])
+
+            if rssi is not None:  # ACK is valid
+                self.rx_ack == maybe_ack
+
+                delta = time.ticks_diff(maybe_ack.ticks_ms, sent_at)
+                print(
+                    f"ACKed with RSSI {rssi}, {delta}ms after sent "
+                    + f"(skew {delta - ACK_DELAY_MS - ack_packet_ms}ms)"
+                )
+
+                if adjust_output_power:
+                    if rssi > RSSI_STRONG_THRESH:
+                        self.adjust_output_power(-1)
+                    elif rssi < RSSI_WEAK_THRESH:
+                        self.adjust_output_power(1)
+
+                return True
+
+            # Otherwise, prepare to sleep briefly and then retry
+            next_try_at = time.ticks_add(sent_at, timeout)
+            sleep_time = time.ticks_diff(next_try_at, time.ticks_ms()) + random.randrange(
+                RETRY_JITTER_MS
+            )
+            if sleep_time > 0:
+                self.modem.sleep()
+                time.sleep_ms(sleep_time)  # TODO: see if this can be machine.lightsleep
+
+            # add 25% timeout for next iteration
+            timeout = (timeout * 5) // 4
+
+        print(f"Failed, no ACK after {MAX_RETRIES} retries.")
+        if adjust_output_power:
+            self.adjust_output_power(2)
+        self.modem.calibrate_image()  # try and improve the RX sensitivity for next time
+        return False
+
+    def _ack_is_valid(self, maybe_ack, csum):
+        # Private function to verify if the RxPacket held in 'maybe_ack' is a valid ACK for the
+        # current device_id and counter value, and provided csum value.
+        #
+        # If it is, returns the reported RSSI value from the packet.
+        # If not, returns None
+        if (not maybe_ack) or len(maybe_ack) != ACK_LENGTH:
+            return None
+
+        base_id, ack_id, ack_counter, ack_csum, rssi = struct.unpack("<HHBBb", maybe_ack)
+
+        if (
+            base_id != RECEIVER_ID
+            or ack_id != self.device_id
+            or ack_counter != self.counter
+            or ack_csum != csum
+        ):
+            return None
+
+        return rssi
+
+    def adjust_output_power(self, delta_dbm):
+        # Adjust the modem output power by +/-delta_dbm, max of OUTPUT_MAX_DBM
+        #
+        # (note: the radio may also apply its own power limit internally.)
+        new = max(min(self.output_power + delta_dbm, OUTPUT_MAX_DBM), OUTPUT_MIN_DBM)
+        self.output_power = new
+        print(f"New output_power {new}/{OUTPUT_MAX_DBM} (delta {delta_dbm})")
+        self.modem.configure({"output_power": self.output_power})
+
+
+if __name__ == "__main__":
+    main()
diff --git a/micropython/lora/examples/reliable_delivery/sender_async.py b/micropython/lora/examples/reliable_delivery/sender_async.py
new file mode 100644
index 000000000..4c14d6f11
--- /dev/null
+++ b/micropython/lora/examples/reliable_delivery/sender_async.py
@@ -0,0 +1,205 @@
+# MicroPython lora reliable_delivery example - asynchronous sender program
+# MIT license; Copyright (c) 2023 Angus Gratton
+import machine
+from machine import SPI, Pin
+import random
+import struct
+import time
+import asyncio
+
+from lora_rd_settings import RECEIVER_ID, ACK_LENGTH, ACK_DELAY_MS, lora_cfg
+
+SLEEP_BETWEEN_MS = 5000  # Main loop should sleep this long between sending data to the receiver
+
+MAX_RETRIES = 4  # Retry each message this often if no ACK is received
+
+# Initial retry is after this long. Increases by 1.25x each subsequent retry.
+BASE_RETRY_TIMEOUT_MS = 1000
+
+# Add random jitter to each retry period, up to this long. Useful to prevent two
+# devices ending up in sync.
+RETRY_JITTER_MS = 1500
+
+# If reported RSSI value is lower than this, increase
+# output power 1dBm
+RSSI_WEAK_THRESH = -110
+
+# If reported RSSI value is higher than this, decrease
+# output power 1dBm
+RSSI_STRONG_THRESH = -70
+
+# IMPORTANT: Set this to the maximum output power in dBm that is permitted in
+# your regulatory environment.
+OUTPUT_MAX_DBM = 15
+OUTPUT_MIN_DBM = -20
+
+
+def get_async_modem():
+    # from lora import AsyncSX1276
+    # return AsyncSX1276(
+    #     spi=SPI(1, baudrate=2000_000, polarity=0, phase=0,
+    #             miso=Pin(19), mosi=Pin(27), sck=Pin(5)),
+    #     cs=Pin(18),
+    #     dio0=Pin(26),
+    #     dio1=Pin(35),
+    #     reset=Pin(14),
+    #     lora_cfg=lora_cfg,
+    # )
+    raise NotImplementedError("Replace this function with one that returns a lora modem instance")
+
+
+def main():
+    modem = get_async_modem()
+    asyncio.run(sender_task(modem))
+
+
+async def sender_task(modem):
+    # Unique ID of this sender, 16-bit number. This method of generating an ID is pretty crummy,
+    # if using this in a real application then probably better to store these in the filesystem or
+    # something like that
+    DEVICE_ID = sum(b for b in machine.unique_id()) & 0xFFFF
+
+    sender = AsyncSender(modem, DEVICE_ID)
+    while True:
+        sensor_data = await get_sensor_data()
+        await sender.send(sensor_data)
+
+        # Sleep until the next time we should read the sensor data and send it to
+        # the receiver. awaiting here means other tasks will run.
+        modem.sleep()
+        await asyncio.sleep_ms(SLEEP_BETWEEN_MS)
+
+
+async def get_sensor_data():
+    # Return a bytes object with the latest sensor data to send to the receiver.
+    #
+    # As this is just an example, we send a dummy payload which is just a string
+    # containing our ticks_ms() timestamp.
+    #
+    # In a real application the sensor data should usually be binary data and
+    # not a string, to save transmission size.
+    return f"Hello, ticks_ms={time.ticks_ms()}".encode()
+
+
+class AsyncSender:
+    def __init__(self, modem, device_id):
+        self.modem = modem
+        self.device_id = device_id
+        self.counter = 0
+        self.output_power = lora_cfg["output_power"]  # start with common settings power level
+        self.rx_ack = None  # reuse the ack message object when we can
+
+        print(f"Sender initialized with ID {device_id:#x}")
+        random.seed(device_id)
+        self.adjust_output_power(0)  # set the initial value within MIN/MAX
+
+        modem.calibrate()
+
+    async def send(self, sensor_data, adjust_output_power=True):
+        # Send a packet of sensor data to the receiver reliably.
+        #
+        # Returns True if data was successfully sent and ACKed, False otherwise.
+        #
+        # If adjust_output_power==True then increase or decrease output power
+        # according to the RSSI reported in the ACK packet.
+        self.counter = (self.counter + 1) & 0xFF
+
+        # Prepare the simple payload with header and checksum
+        # See README for a summary of the simple data message format
+        payload = bytearray(len(sensor_data) + 5)
+        struct.pack_into("<HBB", payload, 0, self.device_id, self.counter, len(sensor_data))
+        payload[4:-1] = sensor_data
+        payload[-1] = sum(b for b in payload) & 0xFF
+
+        # Calculate the time on air (in milliseconds) for an ACK packet
+        ack_packet_ms = self.modem.get_time_on_air_us(ACK_LENGTH) // 1000 + 1
+        timeout = BASE_RETRY_TIMEOUT_MS
+
+        print(f"Sending {len(payload)} bytes")
+
+        # Send the payload, until we receive an acknowledgement or run out of retries
+        for _ in range(MAX_RETRIES):
+            sent_at = await self.modem.send(payload)
+
+            # We expect the receiver of a valid message to start sending the ACK
+            # approximately ACK_DELAY_MS after receiving the message (to allow
+            # the sender time to reconfigure the modem.)
+            #
+            # We start receiving as soon as we can, but allow up to
+            # ACK_DELAY_MS*2 of total timing leeway - plus the time on air for
+            # the packet itself
+            maybe_ack = await self.modem.recv(
+                ack_packet_ms + ACK_DELAY_MS * 2, rx_packet=self.rx_ack
+            )
+
+            # Check if the packet we received is a valid ACK
+            rssi = self._ack_is_valid(maybe_ack, payload[-1])
+
+            if rssi is not None:  # ACK is valid
+                self.rx_ack == maybe_ack
+
+                delta = time.ticks_diff(maybe_ack.ticks_ms, sent_at)
+                print(
+                    f"ACKed with RSSI {rssi}, {delta}ms after sent "
+                    + f"(skew {delta - ACK_DELAY_MS - ack_packet_ms}ms)"
+                )
+
+                if adjust_output_power:
+                    if rssi > RSSI_STRONG_THRESH:
+                        self.adjust_output_power(-1)
+                    elif rssi < RSSI_WEAK_THRESH:
+                        self.adjust_output_power(1)
+
+                return True
+
+            # Otherwise, prepare to sleep briefly and then retry
+            next_try_at = time.ticks_add(sent_at, timeout)
+            sleep_time = time.ticks_diff(next_try_at, time.ticks_ms()) + random.randrange(
+                RETRY_JITTER_MS
+            )
+            if sleep_time > 0:
+                self.modem.sleep()
+                await asyncio.sleep_ms(sleep_time)
+
+            # add 25% timeout for next iteration
+            timeout = (timeout * 5) // 4
+
+        print(f"Failed, no ACK after {MAX_RETRIES} retries.")
+        if adjust_output_power:
+            self.adjust_output_power(2)
+        self.modem.calibrate_image()  # try and improve the RX sensitivity for next time
+        return False
+
+    def _ack_is_valid(self, maybe_ack, csum):
+        # Private function to verify if the RxPacket held in 'maybe_ack' is a valid ACK for the
+        # current device_id and counter value, and provided csum value.
+        #
+        # If it is, returns the reported RSSI value from the packet.
+        # If not, returns None
+        if (not maybe_ack) or len(maybe_ack) != ACK_LENGTH:
+            return None
+
+        base_id, ack_id, ack_counter, ack_csum, rssi = struct.unpack("<HHBBb", maybe_ack)
+
+        if (
+            base_id != RECEIVER_ID
+            or ack_id != self.device_id
+            or ack_counter != self.counter
+            or ack_csum != csum
+        ):
+            return None
+
+        return rssi
+
+    def adjust_output_power(self, delta_dbm):
+        # Adjust the modem output power by +/-delta_dbm, max of OUTPUT_MAX_DBM
+        #
+        # (note: the radio may also apply its own power limit internally.)
+        new = max(min(self.output_power + delta_dbm, OUTPUT_MAX_DBM), OUTPUT_MIN_DBM)
+        self.output_power = new
+        print(f"New output_power {new}/{OUTPUT_MAX_DBM} (delta {delta_dbm})")
+        self.modem.configure({"output_power": self.output_power})
+
+
+if __name__ == "__main__":
+    main()
diff --git a/micropython/lora/examples/simple_rxtx/README.md b/micropython/lora/examples/simple_rxtx/README.md
new file mode 100644
index 000000000..090d69eeb
--- /dev/null
+++ b/micropython/lora/examples/simple_rxtx/README.md
@@ -0,0 +1,26 @@
+# LoRa Simple RX/TX Example
+
+## Source Files
+
+* `simple_rxtx.py` is a very simple implementation of a program to alternately
+  receive and send LoRa packets.
+* `simple_rxtx_async.py` is the same program implemented using async Python.
+
+## Running the examples
+
+One way to run this example interactively:
+
+1. Install or "freeze in" the necessary lora modem driver package (`lora-sx127x`
+   or `lora-sx126x`) and optionally the `lora-async` package if using the async
+   examples (see main lora `README.md` in the above directory for details).
+3. Edit the program you plan to run and fill in the `get_modem()` function with
+   the correct modem type, pin assignments, etc. for your board (see top-level
+   README).
+4. Change to this directory in a terminal.
+5. Run `mpremote run simple_rxtx.py` or `mpremote run
+   simple_rxtx_async.py` as applicable.
+
+Consult the [mpremote
+documentation](https://docs.micropython.org/en/latest/reference/mpremote.html)
+for an explanation of these commands and the options needed to run two copies of
+`mpremote` on different serial ports at the same time.
diff --git a/micropython/lora/examples/simple_rxtx/simple_rxtx.py b/micropython/lora/examples/simple_rxtx/simple_rxtx.py
new file mode 100644
index 000000000..a5602b655
--- /dev/null
+++ b/micropython/lora/examples/simple_rxtx/simple_rxtx.py
@@ -0,0 +1,51 @@
+# MicroPython lora simple_rxtx example - synchronous API version
+# MIT license; Copyright (c) 2023 Angus Gratton
+import time
+from machine import Pin, SPI
+
+
+def get_modem():
+    # from lora import SX1276
+    #
+    # lora_cfg = {
+    #    "freq_khz": 916000,
+    #    "sf": 8,
+    #    "bw": "500",  # kHz
+    #    "coding_rate": 8,
+    #    "preamble_len": 12,
+    #    "output_power": 0,  # dBm
+    # }
+    #
+    # return SX1276(
+    #     spi=SPI(1, baudrate=2000_000, polarity=0, phase=0,
+    #             miso=Pin(19), mosi=Pin(27), sck=Pin(5)),
+    #     cs=Pin(18),
+    #     dio0=Pin(26),
+    #     dio1=Pin(35),
+    #     reset=Pin(14),
+    #     lora_cfg=lora_cfg,
+    # )
+    raise NotImplementedError("Replace this function with one that returns a lora modem instance")
+
+
+def main():
+    print("Initializing...")
+    modem = get_modem()
+
+    counter = 0
+    while True:
+        print("Sending...")
+        modem.send(f"Hello world from MicroPython #{counter}".encode())
+
+        print("Receiving...")
+        rx = modem.recv(timeout_ms=5000)
+        if rx:
+            print(f"Received: {rx!r}")
+        else:
+            print("Timeout!")
+        time.sleep(2)
+        counter += 1
+
+
+if __name__ == "__main__":
+    main()
diff --git a/micropython/lora/examples/simple_rxtx/simple_rxtx_async.py b/micropython/lora/examples/simple_rxtx/simple_rxtx_async.py
new file mode 100644
index 000000000..9504aad48
--- /dev/null
+++ b/micropython/lora/examples/simple_rxtx/simple_rxtx_async.py
@@ -0,0 +1,60 @@
+# MicroPython lora simple_rxtx example - asynchronous API version
+# MIT license; Copyright (c) 2023 Angus Gratton
+import asyncio
+from machine import Pin, SPI
+
+
+def get_async_modem():
+    # from lora import AsyncSX1276
+    #
+    # lora_cfg = {
+    #    "freq_khz": 916000,
+    #    "sf": 8,
+    #    "bw": "500",  # kHz
+    #    "coding_rate": 8,
+    #    "preamble_len": 12,
+    #    "output_power": 0,  # dBm
+    # }
+    #
+    # return AsyncSX1276(
+    #     spi=SPI(1, baudrate=2000_000, polarity=0, phase=0,
+    #             miso=Pin(19), mosi=Pin(27), sck=Pin(5)),
+    #     cs=Pin(18),
+    #     dio0=Pin(26),
+    #     dio1=Pin(35),
+    #     reset=Pin(14),
+    #     lora_cfg=lora_cfg,
+    # )
+    raise NotImplementedError("Replace this function with one that returns a lora modem instance")
+
+
+async def main_task():
+    modem = get_async_modem()
+    await asyncio.gather(
+        asyncio.create_task(send_coro(modem)),
+        asyncio.create_task(recv_coro(modem)),
+    )
+
+
+async def recv_coro(modem):
+    while True:
+        print("Receiving...")
+        rx = await modem.recv(2000)
+        if rx:
+            print(f"Received: {rx!r}")
+        else:
+            print("Receive timeout!")
+
+
+async def send_coro(modem):
+    counter = 0
+    while True:
+        print("Sending...")
+        await modem.send(f"Hello world from async MicroPython #{counter}".encode())
+        print("Sent!")
+        await asyncio.sleep(5)
+        counter += 1
+
+
+if __name__ == "__main__":
+    asyncio.run(main_task())
diff --git a/micropython/lora/lora-async/lora/async_modem.py b/micropython/lora/lora-async/lora/async_modem.py
new file mode 100644
index 000000000..ce9423f84
--- /dev/null
+++ b/micropython/lora/lora-async/lora/async_modem.py
@@ -0,0 +1,143 @@
+# MicroPython LoRa async modem driver
+# MIT license; Copyright (c) 2023 Angus Gratton
+#
+# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.
+
+import asyncio
+import time
+from micropython import const
+
+# Set to True to get some additional printed debug output.
+_DEBUG = const(False)
+
+# Some "belts and braces" timeouts when using IRQs, to wake up if an IRQ hasn't
+# fired as expected. Also a timeout for how rapidly to poll the modem during TX
+# if no IRQ is enabled.
+#
+# All in milliseconds
+_RX_POLL_WITH_IRQ = const(1000)
+_RX_POLL_NO_IRQ = const(50)
+_TX_POLL_LATE_IRQ = const(10)
+
+
+class AsyncModem:
+    # Mixin-like base class that provides coroutines for modem send, recv & recv_continuous.
+    #
+    # Don't instantiate this class directly, instantiate one of the 'AsyncXYZ'
+    # modem classes defined in the lora module.
+
+    def _after_init(self):
+        # Separate flags for (rx, tx) as only one task can block on a flag
+        self._flags = (asyncio.ThreadSafeFlag(), asyncio.ThreadSafeFlag())
+        self.set_irq_callback(self._callback)
+
+    async def recv(self, timeout_ms=None, rx_length=0xFF, rx_packet=None):
+        # Async function to receive a single LoRa packet, with an optional timeout
+        #
+        # Same API as the "Simple API" synchronous function
+        self._flags[0].clear()
+        will_irq = self.start_recv(timeout_ms, False, rx_length)
+        return await self._recv(will_irq, rx_packet)
+
+    def recv_continuous(self, rx_length=0xFF, rx_packet=None):
+        # Returns an Async Iterator that continuously yields LoRa packets, until
+        # the modem is told to sleep() or standby().
+        #
+        # Once MicroPython has PEP525 support (PR #6668) then this somewhat
+        # fiddly implementation can probably be merged with recv() into a much simpler
+        # single _recv() private coro that either yields or returns, depending on
+        # an argument. The public API can stay the same.
+        will_irq = self.start_recv(None, True, rx_length)
+        return AsyncContinuousReceiver(self, will_irq, rx_packet)
+
+    async def _recv(self, will_irq, rx_packet):
+        # Second half of receiving is implemented here to share code with AsyncContinuousReceiver,
+        # until future refactor is possible (see comment immediately above this one.)
+        rx = True
+        while rx is True:
+            await self._wait(will_irq, 0, _RX_POLL_WITH_IRQ if will_irq else _RX_POLL_NO_IRQ)
+            rx = self.poll_recv(rx_packet)
+        if rx:  # RxPacket instance
+            return rx
+
+    async def send(self, packet, tx_at_ms=None):
+        self._flags[1].clear()
+        self.prepare_send(packet)
+
+        timeout_ms = self.get_time_on_air_us(len(packet)) // 1000 + 50
+
+        if tx_at_ms is not None:
+            await asyncio.sleep_ms(max(0, time.ticks_diff(tx_at_ms, time.ticks_ms())))
+
+        if _DEBUG:
+            print("start_send")
+
+        will_irq = self.start_send()
+
+        tx = True
+        while tx is True:
+            await self._wait(will_irq, 1, timeout_ms)
+            tx = self.poll_send()
+
+            if _DEBUG:
+                print(f"poll_send returned tx={tx}")
+
+            # If we've already waited the estimated send time (plus 50ms) and the modem
+            # is not done, timeout and poll more rapidly from here on (unsure if
+            # this is necessary, outside of a serious bug, but doesn't hurt.)
+            timeout_ms = _TX_POLL_LATE_IRQ
+
+        return tx
+
+    async def _wait(self, will_irq, idx, timeout_ms):
+        # Common code path for blocking on an interrupt, if configured.
+        #
+        # idx is into _flags tuple and is 0 for rx and 1 for tx
+        #
+        # timeout_ms is the expected send time for sends, or a reasonable
+        # polling timeout for receives.
+        if _DEBUG:
+            print(f"wait will_irq={will_irq} timeout_ms={timeout_ms}")
+        if will_irq:
+            # In theory we don't need to ever timeout on the flag as the ISR will always
+            # fire, but this gives a workaround for possible race conditions or dropped interrupts.
+            try:
+                await asyncio.wait_for_ms(self._flags[idx].wait(), timeout_ms)
+            except asyncio.TimeoutError:
+                pass
+        else:
+            await asyncio.sleep_ms(timeout_ms)
+
+        if _DEBUG:
+            print("wait complete")
+
+    def _callback(self):
+        # IRQ callback from BaseModem._radio_isr. May be in Hard IRQ context.
+        #
+        # Set both RX & TX flag. This isn't necessary for "real" interrupts, but may be necessary
+        # to wake both for the case of a "soft" interrupt triggered by sleep() or standby(), where
+        # both tasks need to unblock and return.
+        #
+        # The poll_recv() method is a no-op if _tx is set, so waking up both
+        # blocked tasks doesn't have much overhead (no second polling of modem
+        # IRQ flags, for example).
+        for f in self._flags:
+            f.set()
+
+
+class AsyncContinuousReceiver:
+    # Stop-gap async iterator implementation until PEP525 support comes in, see
+    # longer comment  on AsyncModem.recv_continuous() above.
+    def __init__(self, modem, will_irq, rx_packet):
+        self.modem = modem
+        self.will_irq = will_irq
+        self.rx_packet = rx_packet
+
+    def __aiter__(self):
+        return self
+
+    async def __anext__(self):
+        res = await self.modem._recv(self.will_irq, self.rx_packet)
+        if not res:
+            raise StopAsyncIteration
+        return res
diff --git a/micropython/lora/lora-async/manifest.py b/micropython/lora/lora-async/manifest.py
new file mode 100644
index 000000000..1936a50e4
--- /dev/null
+++ b/micropython/lora/lora-async/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.1")
+require("lora")
+package("lora")
diff --git a/micropython/lora/lora-stm32wl5/lora/stm32wl5.py b/micropython/lora/lora-stm32wl5/lora/stm32wl5.py
new file mode 100644
index 000000000..726f1dd53
--- /dev/null
+++ b/micropython/lora/lora-stm32wl5/lora/stm32wl5.py
@@ -0,0 +1,136 @@
+# MicroPython LoRa STM32WL55 embedded sub-ghz radio driver
+# MIT license; Copyright (c) 2022 Angus Gratton
+#
+# This driver is essentially an embedded SX1262 with a custom internal interface block.
+# Requires the stm module in MicroPython to be compiled with STM32WL5 subghz radio support.
+#
+# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.
+from machine import Pin, SPI
+import stm
+from . import sx126x
+from micropython import const
+
+_CMD_CLR_ERRORS = const(0x07)
+
+_REG_OCP = const(0x8E7)
+
+# Default antenna switch config is as per Nucleo WL-55 board. See UM2592 Fig 18.
+# Possible to work with other antenna switch board configurations by passing
+# different ant_sw_class arguments to the modem, any class that creates an object with rx/tx
+
+
+class NucleoWL55RFConfig:
+    def __init__(self):
+        self._FE_CTRL = (Pin(x, mode=Pin.OUT) for x in ("C4", "C5", "C3"))
+
+    def _set_fe_ctrl(self, values):
+        for pin, val in zip(self._FE_CTRL, values):
+            pin(val)
+
+    def rx(self):
+        self._set_fe_ctrl((1, 0, 1))
+
+    def tx(self, hp):
+        self._set_fe_ctrl((0 if hp else 1, 1, 1))
+
+    def idle(self):
+        pass
+
+
+class DIO1:
+    # Dummy DIO1 "Pin" wrapper class to pass to the _SX126x class
+    def irq(self, handler, _):
+        stm.subghz_irq(handler)
+
+
+class _WL55SubGhzModem(sx126x._SX126x):
+    # Don't construct this directly, construct lora.WL55SubGhzModem or lora.AsyncWL55SubGHzModem
+    def __init__(
+        self,
+        lora_cfg=None,
+        tcxo_millivolts=1700,
+        ant_sw=NucleoWL55RFConfig,
+    ):
+        self._hp = False
+
+        if ant_sw == NucleoWL55RFConfig:
+            # To avoid the default argument being an object instance
+            ant_sw = NucleoWL55RFConfig()
+
+        super().__init__(
+            # RM0453 7.2.13 says max 16MHz, but this seems more stable
+            SPI("SUBGHZ", baudrate=8_000_000),
+            stm.subghz_cs,
+            stm.subghz_is_busy,
+            DIO1(),
+            False,  # dio2_rf_sw
+            tcxo_millivolts,  # dio3_tcxo_millivolts
+            10_000,  # dio3_tcxo_start_time_us, first time after POR is quite long
+            None,  # reset
+            lora_cfg,
+            ant_sw,
+        )
+
+    def _clear_errors(self):
+        # A weird difference between STM32WL55 and SX1262, WL55 only takes one
+        # parameter byte for the Clr_Error() command compared to two on SX1262.
+        # The bytes are always zero in both cases.
+        #
+        # (Not clear if sending two bytes will also work always/sometimes, but
+        # sending one byte to SX1262 definitely does not work!
+        self._cmd("BB", _CMD_CLR_ERRORS, 0x00)
+
+    def _clear_irq(self, clear_bits=0xFFFF):
+        super()._clear_irq(clear_bits)
+        # SUBGHZ Radio IRQ requires manual re-enabling after interrupt
+        stm.subghz_irq(self._radio_isr)
+
+    def _tx_hp(self):
+        # STM32WL5 supports both High and Low Power antenna pins depending on tx_ant setting
+        return self._hp
+
+    def _get_pa_tx_params(self, output_power, tx_ant):
+        # Given an output power level in dBm and the tx_ant setting (if any),
+        # return settings for SetPaConfig and SetTxParams.
+        #
+        # ST document RM0453 Set_PaConfig() reference and accompanying Table 35
+        # show values that are an exact superset of the SX1261 and SX1262
+        # available values, depending on which antenna pin is to be
+        # used. Therefore, call either modem's existing _get_pa_tx_params()
+        # function depending on the current tx_ant setting (default is low
+        # power).
+
+        if tx_ant is not None:
+            # Note: currently HP antenna power output is less than it should be,
+            # due to some (unknown) bug.
+            self._hp = tx_ant == "PA_BOOST"
+
+        # Update the OCP register to match the maximum power level
+        self._reg_write(_REG_OCP, 0x38 if self._hp else 0x18)
+
+        if self._hp:
+            return sx126x._SX1262._get_pa_tx_params(self, output_power, tx_ant)
+        else:
+            return sx126x._SX1261._get_pa_tx_params(self, output_power, tx_ant)
+
+
+# Define the actual modem classes that use the SyncModem & AsyncModem "mixin-like" classes
+# to create sync and async variants.
+
+try:
+    from .sync_modem import SyncModem
+
+    class WL55SubGhzModem(_WL55SubGhzModem, SyncModem):
+        pass
+
+except ImportError:
+    pass
+
+try:
+    from .async_modem import AsyncModem
+
+    class AsyncWL55SubGhzModem(_WL55SubGhzModem, AsyncModem):
+        pass
+
+except ImportError:
+    pass
diff --git a/micropython/lora/lora-stm32wl5/manifest.py b/micropython/lora/lora-stm32wl5/manifest.py
new file mode 100644
index 000000000..8c6fe5c5c
--- /dev/null
+++ b/micropython/lora/lora-stm32wl5/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1")
+require("lora-sx126x")
+package("lora")
diff --git a/micropython/lora/lora-sx126x/lora/sx126x.py b/micropython/lora/lora-sx126x/lora/sx126x.py
new file mode 100644
index 000000000..7fa4896ae
--- /dev/null
+++ b/micropython/lora/lora-sx126x/lora/sx126x.py
@@ -0,0 +1,902 @@
+# MicroPython LoRa SX126x (SX1261, SX1262) driver
+# MIT license; Copyright (c) 2023 Angus Gratton
+#
+# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.
+#
+# In comments, abbreviation "DS" = Semtech SX1261/62 Datasheet Rev 2.1 (December 2021)
+import struct
+import time
+from micropython import const
+from machine import Pin
+from .modem import BaseModem, ConfigError, RxPacket, _clamp, _flag
+
+# Set _DEBUG to const(True) to print all SPI commands sent to the device, and all responses,
+# plus a few additional pieces of information.
+_DEBUG = const(False)
+
+_REG_RXGAINCR = const(0x8AC)  # Reset value 0x94
+_REG_LSYNCRH = const(0x740)
+_REG_LSYNCRL = const(0x741)
+
+_CMD_CFG_DIO_IRQ = const(0x08)  # args: IrqMask, Irq1Mask, Irq2Mask,. Irq3Mask
+_CMD_CLR_ERRORS = const(0x07)
+_CMD_CLR_IRQ_STATUS = const(0x02)  # no args
+_CMD_GET_ERROR = const(0x17)
+_CMD_GET_IRQ_STATUS = const(0x12)  # args: (r) Status, IrqStatus
+_CMD_GET_RX_BUFFER_STATUS = const(0x13)  # args: (r) Status, RxPayloadLength, RxBufferPointer
+# NOTE: _CMD_GET_STATUS seems to have an issue, see _get_status() function below.
+_CMD_GET_STATUS = const(0xC0)  # args: (r) Status
+_CMD_GET_PACKET_STATUS = const(0x14)
+_CMD_READ_REGISTER = const(0x1D)  # args: addr (2b), status, (r) Data0 ... DataN
+_CMD_READ_BUFFER = const(0x1E)  # args: Offset, (r) Status, Data0 ... DataN
+_CMD_SET_BUFFER_BASE_ADDRESS = const(0x8F)  # args: TxBaseAddr, RxBaseAddr
+_CMD_SET_MODULATION_PARAMS = const(0x8B)  # args (LoRa): Sf, Bw, Cr, Ldro
+_CMD_SET_PACKET_PARAMS = const(
+    0x8C
+)  # args (LoRa): PbLength, HeaderType, PayloadLength, CrcType, InvertIQ
+_CMD_SET_PACKET_TYPE = const(0x8A)  # args: PktType
+_CMD_SET_PA_CONFIG = const(0x95)  # args: PaDutyCycle, HpMax, HpSel, 0x01
+_CMD_SET_RF_FREQUENCY = const(0x86)  # args: RfFreg
+_CMD_SET_RX = const(0x82)  # args: Timeout
+_CMD_SET_SLEEP = const(0x84)  # args: SleepCfg
+_CMD_SET_STANDBY = const(0x80)  # args: StandbyCfg
+_CMD_SET_DIO3_AS_TCXO_CTRL = const(0x97)  # args: Trim, Timeout (3b)
+_CMD_SET_DIO2_AS_RF_SWITCH_CTRL = const(0x9D)
+_CMD_SET_TX = const(0x83)  # args: Timeout
+_CMD_SET_TX_PARAMS = const(0x8E)  # args: Power, RampTime
+_CMD_WRITE_BUFFER = const(0x0E)  # args: Offset, Data0 ... DataN
+_CMD_WRITE_REGISTER = const(0x0D)  # args: Addr, Data0 ... Data N
+
+_CMD_CALIBRATE = const(0x89)
+_CMD_CALIBRATE_IMAGE = const(0x98)
+
+_STATUS_MODE_MASK = const(0x7 << 4)
+_STATUS_MODE_SHIFT = const(4)
+_STATUS_MODE_STANDBY_RC = const(0x2)
+_STATUS_MODE_STANDBY_HSE32 = const(0x3)
+_STATUS_MODE_FS = const(0x4)
+_STATUS_MODE_RX = const(0x5)
+_STATUS_MODE_TX = const(0x6)
+
+_STATUS_CMD_MASK = const(0x6)  # bits 1-3, bit 0 is reserved
+_STATUS_CMD_SHIFT = const(1)
+_STATUS_CMD_DATA_AVAIL = const(0x2)
+_STATUS_CMD_TIMEOUT = const(0x3)
+_STATUS_CMD_ERROR = const(0x4)
+_STATUS_CMD_EXEC_FAIL = const(0x5)
+_STATUS_CMD_TX_COMPLETE = const(0x6)
+
+_CFG_SF_MIN = const(6)  # inclusive
+_CFG_SF_MAX = const(12)  # inclusive
+
+_IRQ_TX_DONE = const(1 << 0)
+_IRQ_RX_DONE = const(1 << 1)
+_IRQ_PREAMBLE_DETECTED = const(1 << 2)
+_IRQ_SYNC_DETECTED = const(1 << 3)
+_IRQ_HEADER_VALID = const(1 << 4)
+_IRQ_HEADER_ERR = const(1 << 5)
+_IRQ_CRC_ERR = const(1 << 6)
+_IRQ_CAD_DONE = const(1 << 7)
+_IRQ_CAD_DETECTED = const(1 << 8)
+_IRQ_TIMEOUT = const(1 << 9)
+
+# Register values
+_REG_IQ_POLARITY_SETUP = const(0x0736)
+_REG_RX_GAIN = const(0x08AC)
+_REG_RTC_CTRL = const(0x0902)  # DS 15.3 has a typo on this value! Confirmed from Semtech driver
+_REG_EVT_CLR = const(0x0944)
+_REG_EVT_CLR_MASK = const(0x02)
+
+# IRQs the driver cares about when receiving
+_IRQ_DRIVER_RX_MASK = const(_IRQ_RX_DONE | _IRQ_TIMEOUT | _IRQ_CRC_ERR | _IRQ_HEADER_ERR)
+
+
+# Except when entering/waking from sleep, typical busy period <105us (ref RM0453 Table 33)
+#
+# However, if dio3_tcxo_start_time_us is set then can take a longer
+# time to become valid, so a field is set on the modem object with the full timeout.
+#
+# In any case, timeouts here are to catch broken/bad hardware or massive driver
+# bugs rather than commonplace issues.
+#
+_CMD_BUSY_TIMEOUT_BASE_US = const(7000)
+
+# Datasheet says 3.5ms needed to run a full Calibrate command (all blocks),
+# however testing shows it can be as much as as 18ms.
+_CALIBRATE_TYPICAL_TIME_US = const(3500)
+_CALIBRATE_TIMEOUT_US = const(30000)
+
+# Magic value used by SetRx command to indicate a continuous receive
+_CONTINUOUS_TIMEOUT_VAL = const(0xFFFFFF)
+
+
+class _SX126x(BaseModem):
+    # common IRQ masks used by the base class functions
+    _IRQ_RX_COMPLETE = _IRQ_RX_DONE | _IRQ_TIMEOUT
+    _IRQ_TX_COMPLETE = _IRQ_TX_DONE
+
+    # Common base class for SX1261, SX1262 and (pending) STM32WL55. These are all basically
+    # the same except for which PA ranges are supported
+    #
+    # Don't construct this directly, construct lora.SX1261, lora.SX1262, lora.AsyncSX1261
+    # or lora.AsyncSX1262
+    def __init__(
+        self,
+        spi,
+        cs,
+        busy,
+        dio1,
+        dio2_rf_sw,
+        dio3_tcxo_millivolts,
+        dio3_tcxo_start_time_us,
+        reset,
+        lora_cfg,
+        ant_sw,
+    ):
+        super().__init__(ant_sw)
+
+        self._spi = spi
+        self._cs = cs
+        self._busy = busy
+        self._sleep = True  # assume the radio is in sleep mode to start, will wake on _cmd
+        self._dio1 = dio1
+
+        if hasattr(busy, "init"):
+            busy.init(Pin.IN)
+        if hasattr(cs, "init"):
+            cs.init(Pin.OUT, value=1)
+        if hasattr(dio1, "init"):
+            dio1.init(Pin.IN)
+
+        self._busy_timeout = _CMD_BUSY_TIMEOUT_BASE_US + (
+            dio3_tcxo_start_time_us if dio3_tcxo_millivolts else 0
+        )
+
+        self._buf_view = memoryview(bytearray(9))  # shared buffer for commands
+
+        # These settings are kept in the object (as can't read them back from the modem)
+        self._output_power = 14
+        self._bw = 125
+        assert self._bw_hz == 125000  # This field is set in base class, must match self._bw
+
+        # RampTime register value
+        # 0x02 is 40us, default value appears undocumented but this is the SX1276 default
+        self._ramp_val = 0x02
+
+        # Configure the SX126x at least once after reset
+        self._configured = False
+
+        if reset:
+            # If the caller supplies a reset pin argument, reset the radio
+            reset.init(Pin.OUT, value=0)
+            time.sleep_ms(1)
+            reset(1)
+            time.sleep_ms(5)
+        else:
+            # Otherwise, at least put the radio to a known state
+            self._cmd("BB", _CMD_SET_STANDBY, 0)  # STDBY_RC mode, not ready for TCXO yet
+
+        status = self._get_status()
+        if (status[0] != _STATUS_MODE_STANDBY_RC and status[0] != _STATUS_MODE_STANDBY_HSE32) or (
+            status[1] > 1
+        ):
+            # This check exists to determine that the SPI settings and modem
+            # selection are correct. Otherwise it's possible for the driver to
+            # run for quite some time before it detects an invalid response.
+            raise RuntimeError("Invalid initial status {}.".format(status))
+
+        if dio2_rf_sw:
+            self._cmd("BB", _CMD_SET_DIO2_AS_RF_SWITCH_CTRL, 1)
+
+        if dio3_tcxo_millivolts:
+            # Enable TCXO power via DIO3, if enabled
+            #
+            # timeout register is set in units of 15.625us each, use integer math
+            # to calculate and round up:
+            timeout = (dio3_tcxo_start_time_us * 1000 + 15624) // 15625
+            if timeout < 0 or timeout > 1 << 24:
+                raise ValueError("{} out of range".format("dio3_tcxo_start_time_us"))
+            if dio3_tcxo_millivolts < 1600 or dio3_tcxo_millivolts > 3300:
+                raise ValueError("{} out of range".format("dio3_tcxo_millivolts"))
+            dv = dio3_tcxo_millivolts // 100  # 16 to 33
+            tcxo_trim_lookup = (
+                16,
+                17,
+                18,
+                22,
+                24,
+                27,
+                30,
+                33,
+            )  # DS Table 13-35
+            while dv not in tcxo_trim_lookup:
+                dv -= 1
+            reg_tcxo_trim = tcxo_trim_lookup.index(dv)
+
+            self._cmd(">BI", _CMD_SET_DIO3_AS_TCXO_CTRL, (reg_tcxo_trim << 24) + timeout)
+            time.sleep_ms(15)
+            # As per DS 13.3.6 SetDIO3AsTCXOCtrl, should expect error
+            # value 0x20 "XOSC_START_ERR" to be flagged as XOSC has only just
+            # started now. So clear it.
+            self._clear_errors()
+
+            self._check_error()
+
+        # If DIO1 is set, mask in just the IRQs that the driver may need to be
+        # interrupted by. This is important because otherwise an unrelated IRQ
+        # can trigger the ISR and may not be reset by the driver, leaving DIO1 high.
+        #
+        # If DIO1 is not set, all IRQs can stay masked which is the power-on state.
+        if dio1:
+            # Note: we set both Irq mask and DIO1 mask to the same value, which is redundant
+            # (one could be 0xFFFF) but may save a few bytes of bytecode.
+            self._cmd(
+                ">BHHHH",
+                _CMD_CFG_DIO_IRQ,
+                (_IRQ_RX_DONE | _IRQ_TX_DONE | _IRQ_TIMEOUT),  # IRQ mask
+                (_IRQ_RX_DONE | _IRQ_TX_DONE | _IRQ_TIMEOUT),  # DIO1 mask
+                0x0,  # DIO2Mask, not used
+                0x0,  # DIO3Mask, not used
+            )
+            dio1.irq(self._radio_isr, Pin.IRQ_RISING)
+
+        self._clear_irq()
+
+        self._cmd("BB", _CMD_SET_PACKET_TYPE, 1)  # LoRa
+
+        if lora_cfg:
+            self.configure(lora_cfg)
+
+    def sleep(self, warm_start=True):
+        # Put the modem into sleep mode. Driver will wake the modem automatically the next
+        # time an operation starts, or call standby() to wake it manually.
+        #
+        # If the warm_start parameter is False (non-default) then the modem will
+        # lose all settings on wake. The only way to use this parameter value is
+        # to destroy this modem object after calling it, and then instantiate a new
+        # modem object on wake.
+        #
+        self._check_error()  # check errors before going to sleep because we clear on wake
+        self.standby()  # save some code size, this clears the driver's rx/tx state
+        self._cmd("BB", _CMD_SET_SLEEP, _flag(1 << 2, warm_start))
+        self._sleep = True
+
+    def _standby(self):
+        # Send the command for standby mode.
+        #
+        # **Don't call this function directly, call standby() instead.**
+        #
+        # (This private version doesn't update the driver's internal state.)
+        self._cmd("BB", _CMD_SET_STANDBY, 1)  # STDBY_XOSC mode
+        self._clear_irq()  # clear IRQs in case we just cancelled a send or receive
+
+    def is_idle(self):
+        # Returns True if the modem is idle (either in standby or in sleep).
+        #
+        # Note this function can return True in the case where the modem has temporarily gone to
+        # standby but there's a receive configured in software that will resume receiving the next
+        # time poll_recv() or poll_send() is called.
+        if self._sleep:
+            return True  # getting status wakes from sleep
+        mode, _ = self._get_status()
+        return mode in (_STATUS_MODE_STANDBY_HSE32, _STATUS_MODE_STANDBY_RC)
+
+    def _wakeup(self):
+        # Wake the modem from sleep. This is called automatically the first
+        # time a modem command is sent after sleep() was called to put the modem to
+        # sleep.
+        #
+        # To manually wake the modem without initiating a new operation, call standby().
+        self._cs(0)
+        time.sleep_us(20)
+        self._cs(1)
+        self._sleep = False
+        self._clear_errors()  # Clear "XOSC failed to start" which will reappear at this time
+        self._check_error()  # raise an exception if any other error appears
+
+    def _decode_status(self, raw_status, check_errors=True):
+        # split the raw status, which often has reserved bits set, into the mode value
+        # and the command status value
+        mode = (raw_status & _STATUS_MODE_MASK) >> _STATUS_MODE_SHIFT
+        cmd = (raw_status & _STATUS_CMD_MASK) >> _STATUS_CMD_SHIFT
+        if check_errors and cmd in (_STATUS_CMD_EXEC_FAIL, _STATUS_CMD_ERROR):
+            raise RuntimeError("Status {},{} indicates command error".format(mode, cmd))
+        return (mode, cmd)
+
+    def _get_status(self):
+        # Issue the GetStatus command and return the decoded status of (mode
+        # value, command status)
+        #
+        # Due to what appears to be a silicon bug, we send GetIrqStatus here
+        # instead of GetStatus. It seems that there is some specific sequence
+        # where sending command GetStatus to the chip immediately after SetRX
+        # (mode 5) will trip it it into an endless TX (mode 6) for no apparent
+        # reason!
+        #
+        # It doesn't seem to be timing dependent, all that's needed is that
+        # ordering (and the modem works fine otherwise).
+        #
+        # As a workaround we send the GetIrqStatus command and read an extra two
+        # bytes that are then ignored...
+        res = self._cmd("B", _CMD_GET_IRQ_STATUS, n_read=3)[0]
+        return self._decode_status(res)
+
+    def _check_error(self):
+        # Raise a RuntimeError if the radio has reported an error state.
+        #
+        # Return the decoded status, otherwise.
+        res = self._cmd("B", _CMD_GET_ERROR, n_read=3)
+        status = self._decode_status(res[0], False)
+        op_error = (res[1] << 8) + res[2]
+        if op_error != 0:
+            raise RuntimeError("Internal radio Status {} OpError {:#x}".format(status, op_error))
+        self._decode_status(res[0])  # raise an exception here if status shows an error
+        return status
+
+    def _clear_errors(self):
+        # Clear any errors flagged in the modem
+        self._cmd(">BH", _CMD_CLR_ERRORS, 0)
+
+    def _clear_irq(self, clear_bits=0xFFFF):
+        # Clear IRQs flagged in the modem
+        #
+        # By default, clears all IRQ bits. Otherwise, argument is the mask of bits to clear.
+        self._cmd(">BH", _CMD_CLR_IRQ_STATUS, clear_bits)
+        self._last_irq = None
+
+    def _set_tx_ant(self, tx_ant):
+        # Only STM32WL55 allows switching tx_ant from LP to HP
+        raise ConfigError("tx_ant")
+
+    def _symbol_offsets(self):
+        # Called from BaseModem.get_time_on_air_us().
+        #
+        # This function provides a way to implement the different SF5 and SF6 in SX126x,
+        # by returning two offsets: one for the overall number of symbols, and one for the
+        # number of bits used to calculate the symbol length of the payload.
+        return (2, -8) if self._sf in (5, 6) else (0, 0)
+
+    def configure(self, lora_cfg):
+        if self._rx is not False:
+            raise RuntimeError("Receiving")
+
+        if "preamble_len" in lora_cfg:
+            self._preamble_len = lora_cfg["preamble_len"]
+
+        self._invert_iq = [
+            lora_cfg.get("invert_iq_rx", self._invert_iq[0]),
+            lora_cfg.get("invert_iq_tx", self._invert_iq[1]),
+            self._invert_iq[2],
+        ]
+
+        if "freq_khz" in lora_cfg:
+            self._rf_freq_hz = int(lora_cfg["freq_khz"] * 1000)
+            rffreq = (
+                self._rf_freq_hz << 25
+            ) // 32_000_000  # RF-PLL frequency = 32e^6 * RFFreq / 2^25
+            if not rffreq:
+                raise ConfigError("freq_khz")  # set to a value too low
+            self._cmd(">BI", _CMD_SET_RF_FREQUENCY, rffreq)
+
+        if "syncword" in lora_cfg:
+            syncword = lora_cfg["syncword"]
+            if syncword < 0x100:
+                # "Translation from SX127x to SX126x : 0xYZ -> 0xY4Z4 :
+                # if you do not set the two 4 you might lose sensitivity"
+                # see
+                # https://www.thethingsnetwork.org/forum/t/should-private-lorawan-networks-use-a-different-sync-word/34496/15
+                syncword = 0x0404 + ((syncword & 0x0F) << 4) + ((syncword & 0xF0) << 8)
+            self._cmd(">BHH", _CMD_WRITE_REGISTER, _REG_LSYNCRH, syncword)
+
+        if not self._configured or any(
+            key in lora_cfg for key in ("output_power", "pa_ramp_us", "tx_ant")
+        ):
+            pa_config_args, self._output_power = self._get_pa_tx_params(
+                lora_cfg.get("output_power", self._output_power), lora_cfg.get("tx_ant", None)
+            )
+            self._cmd("BBBBB", _CMD_SET_PA_CONFIG, *pa_config_args)
+
+            if "pa_ramp_us" in lora_cfg:
+                self._ramp_val = self._get_pa_ramp_val(
+                    lora_cfg, [10, 20, 40, 80, 200, 800, 1700, 3400]
+                )
+
+            self._cmd("BBB", _CMD_SET_TX_PARAMS, self._output_power, self._ramp_val)
+
+        if not self._configured or any(key in lora_cfg for key in ("sf", "bw", "coding_rate")):
+            if "sf" in lora_cfg:
+                self._sf = lora_cfg["sf"]
+                if self._sf < _CFG_SF_MIN or self._sf > _CFG_SF_MAX:
+                    raise ConfigError("sf")
+
+            if "bw" in lora_cfg:
+                self._bw = lora_cfg["bw"]
+
+            if "coding_rate" in lora_cfg:
+                self._coding_rate = lora_cfg["coding_rate"]
+                if self._coding_rate < 4 or self._coding_rate > 8:  # 4/4 through 4/8, linearly
+                    raise ConfigError("coding_rate")
+
+            bw_val, self._bw_hz = {
+                "7.8": (0x00, 7800),
+                "10.4": (0x08, 10400),
+                "15.6": (0x01, 15600),
+                "20.8": (0x09, 20800),
+                "31.25": (0x02, 31250),
+                "41.7": (0x0A, 41700),
+                "62.5": (0x03, 62500),
+                "125": (0x04, 125000),
+                "250": (0x05, 250000),
+                "500": (0x06, 500000),
+            }[str(self._bw)]
+
+            self._cmd(
+                "BBBBB",
+                _CMD_SET_MODULATION_PARAMS,
+                self._sf,
+                bw_val,
+                self._coding_rate - 4,  # 4/4=0, 4/5=1, etc
+                self._get_ldr_en(),  # Note: BaseModem.get_n_symbols_x4() depends on this logic
+            )
+
+        if "rx_boost" in lora_cfg:
+            # See DS Table 9-3 "Rx Gain Configuration"
+            self._reg_write(_REG_RX_GAIN, 0x96 if lora_cfg["rx_boost"] else 0x94)
+
+        self._check_error()
+        self._configured = True
+
+    def _invert_workaround(self, enable):
+        # Apply workaround for DS 15.4 Optimizing the Inverted IQ Operation
+        if self._invert_iq[2] != enable:
+            val = self._reg_read(_REG_IQ_POLARITY_SETUP)
+            val = (val & ~4) | _flag(4, enable)
+            self._reg_write(_REG_IQ_POLARITY_SETUP, val)
+            self._invert_iq[2] = enable
+
+    def _get_irq(self):
+        # Get currently set IRQ bits.
+        irq_status = self._cmd("B", _CMD_GET_IRQ_STATUS, n_read=3)
+        status = self._decode_status(irq_status[0])
+        flags = (irq_status[1] << 8) + irq_status[2]
+        if _DEBUG:
+            print("Status {} flags {:#x}".format(status, flags))
+        return flags
+
+    def calibrate(self):
+        # Send the Calibrate command to the radio to calibrate RC oscillators, PLL and ADC.
+        #
+        # See DS 13.1.12 Calibrate Function
+
+        # calibParam 0xFE means to calibrate all blocks.
+        self._cmd("BB", _CMD_CALIBRATE, 0xFE)
+
+        time.sleep_us(_CALIBRATE_TYPICAL_TIME_US)
+
+        # a falling edge of BUSY indicates calibration is done
+        self._wait_not_busy(_CALIBRATE_TIMEOUT_US)
+
+    def calibrate_image(self):
+        # Send the CalibrateImage command to the modem to improve reception in
+        # the currently configured frequency band.
+        #
+        # See DS 9.2.1 Image Calibration for Specified Frequency Bands
+        # and 13.1.13 CalibrateImage
+
+        mhz = self._rf_freq_hz // 1_000_000
+        if 430 <= mhz <= 440:
+            args = 0x6B6F
+        elif 470 <= mhz <= 510:
+            args = 0x7581
+        elif 779 <= mhz <= 787:
+            args = 0xC1C5
+        elif 863 <= mhz <= 870:
+            args = 0xD7DB
+        elif 902 <= mhz <= 928:
+            args = 0xE1E9
+        else:
+            # DS says "Contact your Semtech representative for the other optimal
+            # calibration settings outside of the given frequency bands"
+            raise ValueError
+
+        self._cmd(">BH", _CMD_CALIBRATE_IMAGE, args)
+
+        # Can't find anythign in Datasheet about how long image calibration
+        # takes or exactly how it signals completion. Assuming it will be
+        # similar to _CMD_CALIBRATE.
+        self._wait_not_busy(_CALIBRATE_TIMEOUT_US)
+
+    def start_recv(self, timeout_ms=None, continuous=False, rx_length=0xFF):
+        # Start receiving.
+        #
+        # Part of common low-level modem API, see README.md for usage.
+        super().start_recv(timeout_ms, continuous, rx_length)  # sets _rx
+
+        if self._tx:
+            # Send is in progress and has priority, _check_recv() will start recv
+            # once send finishes (caller needs to call poll_send() for this to happen.)
+            if _DEBUG:
+                print("Delaying receive until send completes")
+            return self._dio1
+
+        # Put the modem in a known state. It's possible a different
+        # receive was in progress, this prevent anything changing while
+        # we set up the new receive
+        self._standby()  # calling private version to keep driver state as-is
+
+        # Allocate the full FIFO for RX
+        self._cmd("BBB", _CMD_SET_BUFFER_BASE_ADDRESS, 0xFF, 0x0)
+
+        self._cmd(
+            ">BHBBBB",
+            _CMD_SET_PACKET_PARAMS,
+            self._preamble_len,
+            self._implicit_header,
+            rx_length,  # PayloadLength, only used in implicit header mode
+            self._crc_en,  # CRCType, only used in implicit header mode
+            self._invert_iq[0],  # InvertIQ
+        )
+        self._invert_workaround(self._invert_iq[0])
+
+        if continuous:
+            timeout = _CONTINUOUS_TIMEOUT_VAL
+        elif timeout_ms is not None:
+            timeout = max(1, timeout_ms * 64)  # units of 15.625us
+        else:
+            timeout = 0  # Single receive mode, no timeout
+
+        self._cmd(">BBH", _CMD_SET_RX, timeout >> 16, timeout)  # 24 bits
+
+        return self._dio1
+
+    def poll_recv(self, rx_packet=None):
+        old_rx = self._rx
+        rx = super().poll_recv(rx_packet)
+
+        if rx is not True and old_rx is not False and isinstance(old_rx, int):
+            # Receiving has just stopped, and a timeout was previously set.
+            #
+            # Workaround for errata DS 15.3 "Implicit Header Mode Timeout Behaviour",
+            # which recommends to add the following after "ANY Rx with Timeout active sequence"
+            self._reg_write(_REG_RTC_CTRL, 0x00)
+            self._reg_write(_REG_EVT_CLR, self._reg_read(_REG_EVT_CLR) | _REG_EVT_CLR_MASK)
+
+        return rx
+
+    def _rx_flags_success(self, flags):
+        # Returns True if IRQ flags indicate successful receive.
+        # Specifically, from the bits in _IRQ_DRIVER_RX_MASK:
+        # - _IRQ_RX_DONE must be set
+        # - _IRQ_TIMEOUT must not be set
+        # - _IRQ_CRC_ERR must not be set
+        # - _IRQ_HEADER_ERR must not be set
+        #
+        # (Note: this is a function because the result for SX1276 depends on
+        # current config, but the result is constant here.)
+        return flags & _IRQ_DRIVER_RX_MASK == _IRQ_RX_DONE
+
+    def _read_packet(self, rx_packet, flags):
+        # Private function to read received packet (RxPacket object) from the
+        # modem, if there is one.
+        #
+        # Called from poll_recv() function, which has already checked the IRQ flags
+        # and verified a valid receive happened.
+
+        ticks_ms = self._get_last_irq()
+
+        res = self._cmd("B", _CMD_GET_RX_BUFFER_STATUS, n_read=3)
+        rx_payload_len = res[1]
+        rx_buffer_ptr = res[2]  # should be 0
+
+        if rx_packet is None or len(rx_packet) != rx_payload_len:
+            rx_packet = RxPacket(rx_payload_len)
+
+        self._cmd("BB", _CMD_READ_BUFFER, rx_buffer_ptr, n_read=1, read_buf=rx_packet)
+
+        pkt_status = self._cmd("B", _CMD_GET_PACKET_STATUS, n_read=4)
+
+        rx_packet.ticks_ms = ticks_ms
+        # SNR units are dB * 4 (signed)
+        rx_packet.rssi, rx_packet.snr = struct.unpack("xBbx", pkt_status)
+        rx_packet.rssi //= -2  # RSSI, units: dBm
+        rx_packet.crc_error = (flags & _IRQ_CRC_ERR) != 0
+
+        return rx_packet
+
+    def prepare_send(self, packet):
+        # Prepare modem to start sending. Should be followed by a call to start_send()
+        #
+        # Part of common low-level modem API, see README.md for usage.
+        if len(packet) > 255:
+            raise ConfigError("packet too long")
+
+        # Put the modem in a known state. Any current receive is suspended at this point,
+        # but calling _check_recv() will resume it later.
+        self._standby()  # calling private version to keep driver state as-is
+
+        self._check_error()
+
+        # Set the board antenna for correct TX mode
+        if self._ant_sw:
+            self._ant_sw.tx(self._tx_hp())
+
+        self._last_irq = None
+
+        self._cmd(
+            ">BHBBBB",
+            _CMD_SET_PACKET_PARAMS,
+            self._preamble_len,
+            self._implicit_header,
+            len(packet),
+            self._crc_en,
+            self._invert_iq[1],  # _invert_iq_tx
+        )
+        self._invert_workaround(self._invert_iq[1])
+
+        # Allocate the full FIFO for TX
+        self._cmd("BBB", _CMD_SET_BUFFER_BASE_ADDRESS, 0x0, 0xFF)
+        self._cmd("BB", _CMD_WRITE_BUFFER, 0x0, write_buf=packet)
+
+        # Workaround for DS 15.1 Modulation Quality with 500 kHZ LoRa Bandwidth
+        # ... apparently this needs to be done "*before each packet transmission*"
+        if self._bw_hz == 500_000:
+            self._reg_write(0x0889, self._reg_read(0x0889) & 0xFB)
+        else:
+            self._reg_write(0x0889, self._reg_read(0x0889) | 0x04)
+
+    def start_send(self):
+        # Actually start a send that was loaded by calling prepare_send().
+        #
+        # This is split into a separate function to allow more precise timing.
+        #
+        # The driver doesn't verify the caller has done the right thing here, the
+        # modem will no doubt do something weird if prepare_send() was not called!
+        #
+        # Part of common low-level modem API, see README.md for usage.
+
+        # Currently we don't pass any TX timeout argument to the modem1,
+        # which the datasheet ominously offers as "security" for the Host MCU if
+        # the send doesn't start for some reason.
+
+        self._cmd("BBBB", _CMD_SET_TX, 0x0, 0x0, 0x0)
+
+        if _DEBUG:
+            print("status {}".format(self._get_status()))
+        self._check_error()
+
+        self._tx = True
+
+        return self._dio1
+
+    def _wait_not_busy(self, timeout_us):
+        # Wait until the radio de-asserts the busy line
+        start = time.ticks_us()
+        ticks_diff = 0
+        while self._busy():
+            ticks_diff = time.ticks_diff(time.ticks_us(), start)
+            if ticks_diff > timeout_us:
+                raise RuntimeError("BUSY timeout", timeout_us)
+            time.sleep_us(1)
+        if _DEBUG and ticks_diff > 105:
+            # By default, debug log any busy time that takes longer than the
+            # datasheet-promised Typical 105us (this happens when starting the 32MHz oscillator,
+            # if it's turned on and off by the modem, and maybe other times.)
+            print(f"BUSY {ticks_diff}us")
+
+    def _cmd(self, fmt, *write_args, n_read=0, write_buf=None, read_buf=None):
+        # Execute an SX1262 command
+        # fmt - Format string suitable for use with struct.pack. First item should be 'B' and
+        # corresponds to the command opcode.
+        # write_args - Arguments suitable for struct.pack using fmt. First argument should be a
+        # command opcode byte.
+        #
+        # Optional arguments:
+        # write_buf - Extra buffer to write from (for FIFO writes). Mutually exclusive with n_read
+        # or read_buf.
+        # n_read - Number of result bytes to read back at end
+        # read_buf - Extra buffer to read into (for FIFO reads)
+        #
+        # Returns None if n_read==0, otherwise a memoryview of length n_read which points into a
+        # shared buffer (buffer will be clobbered on next call to _cmd!)
+        if self._sleep:
+            self._wakeup()
+
+        # Ensure "busy" from previously issued command has de-asserted. Usually this will
+        # have happened well before _cmd() is called again.
+        self._wait_not_busy(self._busy_timeout)
+
+        # Pack write_args into slice of _buf_view memoryview of correct length
+        wrlen = struct.calcsize(fmt)
+        assert n_read + wrlen <= len(self._buf_view)  # if this fails, make _buf bigger!
+        struct.pack_into(fmt, self._buf_view, 0, *write_args)
+        buf = self._buf_view[: (wrlen + n_read)]
+
+        if _DEBUG:
+            print(">>> {}".format(buf[:wrlen].hex()))
+            if write_buf:
+                print(">>> {}".format(write_buf.hex()))
+        self._cs(0)
+        self._spi.write_readinto(buf, buf)
+        if write_buf:
+            self._spi.write(write_buf)  # Used by _CMD_WRITE_BUFFER only
+        if read_buf:
+            self._spi.readinto(read_buf, 0xFF)  # Used by _CMD_READ_BUFFER only
+        self._cs(1)
+
+        if n_read > 0:
+            res = self._buf_view[wrlen : (wrlen + n_read)]  # noqa: E203
+            if _DEBUG:
+                print("<<< {}".format(res.hex()))
+            return res
+
+    def _reg_read(self, addr):
+        return self._cmd(">BHB", _CMD_READ_REGISTER, addr, 0, n_read=1)[0]
+
+    def _reg_write(self, addr, val):
+        return self._cmd(">BHB", _CMD_WRITE_REGISTER, addr, val & 0xFF)
+
+
+class _SX1262(_SX126x):
+    # Don't construct this directly, construct lora.SX1262 or lora.AsyncSX1262
+    def __init__(
+        self,
+        spi,
+        cs,
+        busy,
+        dio1=None,
+        dio2_rf_sw=True,
+        dio3_tcxo_millivolts=None,
+        dio3_tcxo_start_time_us=1000,
+        reset=None,
+        lora_cfg=None,
+        ant_sw=None,
+    ):
+        super().__init__(
+            spi,
+            cs,
+            busy,
+            dio1,
+            dio2_rf_sw,
+            dio3_tcxo_millivolts,
+            dio3_tcxo_start_time_us,
+            reset,
+            lora_cfg,
+            ant_sw,
+        )
+
+        # Apply workaround for DS 15.2 "Better Resistance of the SX1262 Tx to Antenna Mismatch
+        self._reg_write(0x8D8, self._reg_read(0x8D8) | 0x1E)
+
+    def _tx_hp(self):
+        # SX1262 has High Power only (deviceSel==0)
+        return True
+
+    def _get_pa_tx_params(self, output_power, tx_ant):
+        # Given an output power level in dB, return a 2-tuple:
+        # - First item is the 3 arguments for SetPaConfig command
+        # - Second item is the power level argument value for SetTxParams command.
+        #
+        # DS 13.1.14.1 "PA Optimal Settings" gives optimally efficient
+        # values for output power +22, +20, +17, +14 dBm and "these changes make
+        # the use of nominal power either sub-optimal or unachievable" (hence it
+        # recommends setting +22dBm nominal TX Power for all these).
+        #
+        # However the modem supports output power as low as -9dBm, and there's
+        # no explanation in the datasheet of how to best set other output power
+        # levels.
+        #
+        # Semtech's own driver (sx126x.c in LoRaMac-node) only ever executes
+        # SetPaConfig with the values shown in the datasheet for +22dBm, and
+        # then executes SetTxParams with power set to the nominal value in
+        # dBm.
+        #
+        # Try for best of both worlds here: If the caller requests an "Optimal"
+        # value, use the datasheet values. Otherwise set nominal power only as
+        # per Semtech's driver.
+        output_power = int(_clamp(output_power, -9, 22))
+
+        DEFAULT = (0x4, 0x7, 0x0, 0x1)
+        OPTIMAL = {
+            22: (DEFAULT, 22),
+            20: ((0x3, 0x5, 0x0, 0x1), 22),
+            17: ((0x2, 0x3, 0x0, 0x1), 22),
+            14: ((0x2, 0x2, 0x0, 0x1), 22),
+        }
+        if output_power in OPTIMAL:
+            # Datasheet optimal values
+            return OPTIMAL[output_power]
+        else:
+            # Nominal values, as per Semtech driver
+            return (DEFAULT, output_power & 0xFF)
+
+
+class _SX1261(_SX126x):
+    # Don't construct this directly, construct lora.SX1261, or lora.AsyncSX1261
+    def __init__(
+        self,
+        spi,
+        cs,
+        busy,
+        dio1=None,
+        dio2_rf_sw=True,
+        dio3_tcxo_millivolts=None,
+        dio3_tcxo_start_time_us=1000,
+        reset=None,
+        lora_cfg=None,
+        ant_sw=None,
+    ):
+        super().__init__(
+            spi,
+            cs,
+            busy,
+            dio1,
+            dio2_rf_sw,
+            dio3_tcxo_millivolts,
+            dio3_tcxo_start_time_us,
+            reset,
+            lora_cfg,
+            ant_sw,
+        )
+
+    def _tx_hp(self):
+        # SX1261 has Low Power only (deviceSel==1)
+        return False
+
+    def _get_pa_tx_params(self, output_power, tx_ant):
+        # Given an output power level in dB, return a 2-tuple:
+        # - First item is the 3 arguments for SetPaConfig command
+        # - Second item is the power level argument value for SetTxParams command.
+        #
+        # As noted above for SX1262, DS 13.1.14.1 "PA Optimal Settings"
+        # gives optimally efficient values for output power +15, +14, +10 dBm
+        # but nothing specific to the other power levels (down to -17dBm).
+        #
+        # Therefore do the same as for SX1262 to set optimal values if known, nominal otherwise.
+        output_power = _clamp(int(output_power), -17, 15)
+
+        DEFAULT = (0x4, 0x0, 0x1, 0x1)
+        OPTIMAL = {
+            15: ((0x06, 0x0, 0x1, 0x1), 14),
+            14: (DEFAULT, 14),
+            10: ((0x1, 0x0, 0x1, 0x1), 13),
+        }
+
+        if output_power == 15 and self._rf_freq_hz < 400_000_000:
+            # DS 13.1.14.1 has Note that PaDutyCycle is limited to 0x4 below 400MHz,
+            # so disallow the 15dBm optimal setting.
+            output_power = 14
+
+        if output_power in OPTIMAL:
+            # Datasheet optimal values
+            return OPTIMAL[output_power]
+        else:
+            # Nominal values, as per Semtech driver
+            return (DEFAULT, output_power & 0xFF)
+
+
+# Define the actual modem classes that use the SyncModem & AsyncModem "mixin-like" classes
+# to create sync and async variants.
+
+try:
+    from .sync_modem import SyncModem
+
+    class SX1261(_SX1261, SyncModem):
+        pass
+
+    class SX1262(_SX1262, SyncModem):
+        pass
+
+except ImportError:
+    pass
+
+try:
+    from .async_modem import AsyncModem
+
+    class AsyncSX1261(_SX1261, AsyncModem):
+        pass
+
+    class AsyncSX1262(_SX1262, AsyncModem):
+        pass
+
+except ImportError:
+    pass
diff --git a/micropython/lora/lora-sx126x/manifest.py b/micropython/lora/lora-sx126x/manifest.py
new file mode 100644
index 000000000..76fa91d8d
--- /dev/null
+++ b/micropython/lora/lora-sx126x/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.5")
+require("lora")
+package("lora")
diff --git a/micropython/lora/lora-sx127x/lora/sx127x.py b/micropython/lora/lora-sx127x/lora/sx127x.py
new file mode 100644
index 000000000..9faa79a4d
--- /dev/null
+++ b/micropython/lora/lora-sx127x/lora/sx127x.py
@@ -0,0 +1,889 @@
+# MicroPython LoRa SX127x driver
+# MIT license; Copyright (c) 2023 Angus Gratton
+#
+# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.
+#
+# In comments, abbreviation "DS" = Semtech SX1276/77/78/79 Datasheet rev 7 (May 2020)
+from micropython import const
+from .modem import BaseModem, ConfigError, RxPacket, _clamp, _flag
+from machine import Pin
+import struct
+import time
+
+# Set _DEBUG to const(True) to print all register reads and writes, and current register values
+# even when an update isn't needed. Plus a few additional pieces of information.
+_DEBUG = const(False)
+
+_WRITE_REG_BIT = const(1 << 7)
+
+# Registers and fields as bytecode-zerocost constants
+#
+# Where possible names are direct from DS section 4.4
+# (This means some names are slightly inconsistent, as per datasheet...)
+
+_REG_FIFO = const(0x00)
+
+_REG_OPMODE = const(0x01)
+
+_OPMODE_LONGRANGEMODE_LORA = const(1 << 7)
+_OPMODE_LONGRANGEMODE_FSK_OOK = const(0)
+_OPMODE_MODE_MASK = const(0x7)
+_OPMODE_MODE_SLEEP = const(0x0)
+_OPMODE_MODE_STDBY = const(0x1)
+_OPMODE_MODE_FSTX = const(0x2)  # Frequency synthesis (TX)
+_OPMODE_MODE_TX = const(0x3)
+_OPMODE_MODE_FSRX = const(0x4)  # Frequency synthesis (RX)
+_OPMODE_MODE_RX_CONTINUOUS = const(0x5)
+_OPMODE_MODE_RX_SINGLE = const(0x6)
+_OPMODE_MODE_CAD = const(0x7)  # Channel Activity Detection
+
+_REG_FR_MSB = const(0x06)
+_REG_FR_MID = const(0x07)
+_REG_FR_LSB = const(0x08)
+
+_REG_PA_CONFIG = const(0x09)
+
+_PA_CONFIG_PASELECT_PA_BOOST_PIN = const(1 << 7)
+_PA_CONFIG_PASELECT_RFO_PIN = const(0x0)
+_PA_CONFIG_MAXPOWER_SHIFT = const(0x4)
+_PA_CONFIG_MAXPOWER_MASK = const(0x7)
+_PA_CONFIG_OUTPUTPOWER_SHIFT = const(0)
+_PA_CONFIG_OUTPUTPOWER_MASK = const(0xF)
+
+_REG_PA_RAMP = const(0x0A)
+_PA_RAMP_MASK = const(0x0F)
+
+_REG_LNA = const(0x0C)
+
+_LNA_GAIN_MASK = const(0x7)
+_LNA_GAIN_SHIFT = const(5)
+
+_LNA_BOOST_HF_MASK = 0x3
+_LNA_BOOST_HF_SHIFT = 0x0
+
+_REG_FIFO_ADDR_PTR = const(0x0D)
+_REG_FIFO_TX_BASE_ADDR = const(0x0E)
+_REG_FIFO_RX_BASE_ADDR = const(0x0F)
+_REG_FIFO_RX_CURRENT_ADDR = const(0x10)
+
+_REG_IRQ_FLAGS_MASK = const(0x11)
+_REG_IRQ_FLAGS = const(0x12)
+
+# IRQ mask bits are the same as the IRQ flag bits
+_IRQ_RX_TIMEOUT = const(1 << 7)
+_IRQ_RX_DONE = const(1 << 6)
+_IRQ_PAYLOAD_CRC_ERROR = const(1 << 5)
+_IRQ_VALID_HEADER = const(1 << 4)
+_IRQ_TX_DONE = const(1 << 3)
+_IRQ_CAD_DONE = const(1 << 2)
+_IRQ_FHSS_CHANGE_CHANNEL = const(1 << 1)
+_IRQ_CAD_DETECTED = const(1 << 0)
+
+_REG_RX_NB_BYTES = const(0x13)
+_REG_RX_HEADER_CNT_VALUE_MSB = const(0x14)
+_REG_RX_HEADER_CNT_VALUE_LSB = const(0x13)
+_REG_RX_PACKET_CNT_VALUE_MSB = const(0x16)
+_REG_RX_PACKET_CNT_VALUE_LSB = const(0x17)
+
+_REG_MODEM_STAT = const(0x18)
+_MODEM_STAT_RX_CODING_RATE_MASK = const(0xE)
+_MODEM_STAT_RX_CODING_RATE_SHIFT = const(5)
+_MODEM_STAT_MODEM_CLEAR = const(1 << 4)
+_MODEM_STAT_HEADER_INFO_VALID = const(1 << 3)
+_MODEM_STAT_RX_ONGOING = const(1 << 2)
+_MODEM_STAT_SIGNAL_SYNC = const(1 << 1)  # Signal synchronized
+_MODEM_STAT_SIGNAL_DET = const(1 << 0)  # Signal detected
+
+_REG_PKT_SNR_VAL = const(0x19)
+_REG_PKT_RSSI_VAL = const(0x1A)
+_REG_RSSI_VAL = const(0x1B)
+
+_REG_HOP_CHANNEL = const(0x1C)
+_HOP_CHANNEL_PLL_TIMEOUT = const(1 << 7)
+_HOP_CHANNEL_CRC_ON_PAYLOAD = const(1 << 6)
+_HOP_CHANNEL_FHSS_PRESENT_CHANNEL_MASK = const(0x1F)
+
+_REG_MODEM_CONFIG1 = const(0x1D)
+_MODEM_CONFIG1_BW_MASK = const(0xF)
+_MODEM_CONFIG1_BW_SHIFT = const(4)
+_MODEM_CONFIG1_BW7_8 = const(0x0)
+_MODEM_CONFIG1_BW10_4 = const(0x1)
+_MODEM_CONFIG1_BW15_6 = const(0x2)
+_MODEM_CONFIG1_BW20_8 = const(0x3)
+_MODEM_CONFIG1_BW31_25 = const(0x4)
+_MODEM_CONFIG1_BW41_7 = const(0x5)
+_MODEM_CONFIG1_BW62_5 = const(0x6)
+_MODEM_CONFIG1_BW125 = const(0x7)
+_MODEM_CONFIG1_BW250 = const(0x8)  # not supported in lower band (169MHz)
+_MODEM_CONFIG1_BW500 = const(0x9)  # not supported in lower band (169MHz)
+_MODEM_CONFIG1_CODING_RATE_MASK = const(0x7)
+_MODEM_CONFIG1_CODING_RATE_SHIFT = const(1)
+_MODEM_CONFIG1_CODING_RATE_45 = const(0b001)
+_MODEM_CONFIG1_CODING_RATE_46 = const(0b010)
+_MODEM_CONFIG1_CODING_RATE_47 = const(0b011)
+_MODEM_CONFIG1_CODING_RATE_48 = const(0b100)
+_MODEM_CONFIG1_IMPLICIT_HEADER_MODE_ON = const(1 << 0)
+
+_REG_MODEM_CONFIG2 = const(0x1E)
+_MODEM_CONFIG2_SF_MASK = const(0xF)  # Spreading Factor
+_MODEM_CONFIG2_SF_SHIFT = const(4)
+# SF values are integers 6-12 for SF6-SF12, so skipping constants for these
+_MODEM_CONFIG2_SF_MIN = const(6)  # inclusive
+_MODEM_CONFIG2_SF_MAX = const(12)  # inclusive
+
+_MODEM_CONFIG2_TX_CONTINUOUS = const(1 << 3)
+_MODEM_CONFIG2_RX_PAYLOAD_CRC_ON = const(1 << 2)
+_MODEM_CONFIG2_SYMB_TIMEOUT_MSB_MASK = 0x3
+
+_REG_SYMB_TIMEOUT_LSB = const(0x1F)
+
+_REG_PREAMBLE_LEN_MSB = const(0x20)
+_REG_PREAMBLE_LEN_LSB = const(0x21)
+
+_REG_PAYLOAD_LEN = const(0x22)  # Only for implicit header mode & TX
+_REG_MAX_PAYLOAD_LEN = const(0x23)
+
+_REG_HOP_PERIOD = const(0x24)
+
+_REG_FIFO_TXBYTE_ADDR = const(0x25)
+
+_REG_MODEM_CONFIG3 = const(0x26)
+_MODEM_CONFIG3_AGC_ON = const(1 << 2)
+_MODEM_CONFIG3_LOW_DATA_RATE_OPTIMIZE = const(1 << 3)
+
+_REG_DETECT_OPTIMIZE = const(0x31)
+_DETECT_OPTIMIZE_AUTOMATIC_IF_ON = const(
+    1 << 7
+)  # Bit should be cleared after reset, as per errata
+_DETECT_OPTIMIZE_MASK = 0x7
+_DETECT_OPTIMIZE_SF6 = const(0x05)
+_DETECT_OPTIMIZE_OTHER = const(0x03)
+
+# RegInvertIQ is not correctly documented in DS Rev 7 (May 2020).
+#
+# The correct behaviour for interoperability with other LoRa devices is as
+# written here:
+# https://github.com/eclipse/upm/blob/master/src/sx1276/sx1276.cxx#L1310
+#
+# Same as used in the Semtech mbed driver, here:
+# https://github.com/ARMmbed/mbed-semtech-lora-rf-drivers/blob/master/SX1276/SX1276_LoRaRadio.cpp#L778
+# https://github.com/ARMmbed/mbed-semtech-lora-rf-drivers/blob/master/SX1276/registers/sx1276Regs-LoRa.h#L443
+#
+# Specifically:
+# - The TX bit in _REG_INVERT_IQ is opposite to what's documented in the datasheet
+#   (0x01 normal, 0x00 inverted)
+# - The RX bit in _REG_INVERT_IQ is as documented in the datasheet (0x00 normal, 0x40 inverted)
+# - When enabling LoRa mode, the default register value becomes 0x27 (normal RX & TX)
+#   rather than the documented power-on value of 0x26.
+_REG_INVERT_IQ = const(0x33)
+_INVERT_IQ_RX = const(1 << 6)
+_INVERT_IQ_TX_OFF = const(1 << 0)
+
+_REG_DETECTION_THRESHOLD = const(0x37)
+_DETECTION_THRESHOLD_SF6 = const(0x0C)
+_DETECTION_THRESHOLD_OTHER = const(0x0A)  # SF7 to SF12
+
+_REG_SYNC_WORD = const(0x39)
+
+_REG_FSKOOK_IMAGE_CAL = const(0x3B)  # NOTE: Only accessible in FSK/OOK mode
+_IMAGE_CAL_START = const(1 << 6)
+_IMAGE_CAL_RUNNING = const(1 << 5)
+_IMAGE_CAL_AUTO = const(1 << 7)
+
+_REG_INVERT_IQ2 = const(0x3B)
+_INVERT_IQ2_ON = const(0x19)
+_INVERT_IQ2_OFF = const(0x1D)
+
+_REG_DIO_MAPPING1 = const(0x40)
+_DIO0_MAPPING_MASK = const(0x3)
+_DIO0_MAPPING_SHIFT = const(6)
+_DIO1_MAPPING_MASK = const(0x3)
+_DIO1_MAPPING_SHIFT = const(4)
+_DIO2_MAPPING_MASK = const(0x3)
+_DIO2_MAPPING_SHIFT = const(2)
+_DIO3_MAPPING_MASK = const(0x3)
+_DIO3_MAPPING_SHIFT = const(0)
+
+_REG_DIO_MAPPING2 = const(0x41)
+_DIO4_MAPPING_MASK = const(0x3)
+_DIO4_MAPPING_SHIFT = const(6)
+_DIO5_MAPPING_MASK = const(0x3)
+_DIO5_MAPPING_SHIFT = const(4)
+
+_REG_PA_DAC = const(0x4D)
+_PA_DAC_DEFAULT_VALUE = const(0x84)  # DS 3.4.3 High Power +20 dBm Operation
+_PA_DAC_HIGH_POWER_20DBM = const(0x87)
+
+_REG_VERSION = const(0x42)
+
+# IRQs the driver masks in when receiving
+_IRQ_DRIVER_RX_MASK = const(
+    _IRQ_RX_DONE | _IRQ_RX_TIMEOUT | _IRQ_VALID_HEADER | _IRQ_PAYLOAD_CRC_ERROR
+)
+
+
+class _SX127x(BaseModem):
+    # Don't instantiate this class directly, instantiate either lora.SX1276,
+    #  lora.SX1277, lora.SX1278, lora.SX1279, or lora.AsyncSX1276,
+    #  lora.AsyncSX1277, lora.AsyncSX1278, lora.AsyncSX1279 as applicable.
+
+    # common IRQ masks used by the base class functions
+    _IRQ_RX_COMPLETE = _IRQ_RX_DONE | _IRQ_RX_TIMEOUT
+    _IRQ_TX_COMPLETE = _IRQ_TX_DONE
+
+    def __init__(self, spi, cs, dio0=None, dio1=None, reset=None, lora_cfg=None, ant_sw=None):
+        super().__init__(ant_sw)
+
+        self._buf1 = bytearray(1)  # shared small buffers
+        self._buf2 = bytearray(2)
+        self._spi = spi
+        self._cs = cs
+
+        self._dio0 = dio0
+        self._dio1 = dio1
+
+        cs.init(Pin.OUT, value=1)
+
+        if dio0:
+            dio0.init(Pin.IN)
+            dio0.irq(self._radio_isr, trigger=Pin.IRQ_RISING)
+            if dio1:
+                dio1.init(Pin.IN)
+                dio1.irq(self._radio_isr, trigger=Pin.IRQ_RISING)
+
+        # Configuration settings that need to be tracked by the driver
+        # Note: a number of these are set in the base class constructor
+        self._pa_boost = False
+
+        if reset:
+            # If the user supplies a reset pin argument, reset the radio
+            reset.init(Pin.OUT, value=0)
+            time.sleep_ms(1)
+            reset(1)
+            time.sleep_ms(5)
+
+        version = self._reg_read(_REG_VERSION)
+        if version != 0x12:
+            raise RuntimeError("Unexpected silicon version {}".format(version))
+
+        # wake the radio and enable LoRa mode if it's not already set
+        self._set_mode(_OPMODE_MODE_STDBY)
+
+        if lora_cfg:
+            self.configure(lora_cfg)
+
+    def configure(self, lora_cfg):
+        if self._rx is not False:
+            raise RuntimeError("Receiving")
+
+        # Set frequency
+        if "freq_khz" in lora_cfg:
+            # Assuming F(XOSC)=32MHz (datasheet both implies this value can be different, and
+            # specifies it shouldn't be different!)
+            self._rf_freq_hz = int(lora_cfg["freq_khz"] * 1000)
+            fr_val = self._rf_freq_hz * 16384 // 1000_000
+            buf = bytes([fr_val >> 16, (fr_val >> 8) & 0xFF, fr_val & 0xFF])
+            self._reg_write(_REG_FR_MSB, buf)
+
+        # Turn on/off automatic image re-calibration if temperature changes. May lead to dropped
+        # packets if enabled.
+        if "auto_image_cal" in lora_cfg:
+            self._set_mode(_OPMODE_MODE_STDBY, False)  # Disable LoRa mode to access FSK/OOK
+            self._reg_update(
+                _REG_FSKOOK_IMAGE_CAL,
+                _IMAGE_CAL_AUTO,
+                _flag(_IMAGE_CAL_AUTO, lora_cfg["auto_image_cal"]),
+            )
+            self._set_mode(_OPMODE_MODE_STDBY)  # Switch back to LoRa mode
+
+        # Note: Common pattern below is to generate a new register value and an update_mask,
+        # and then call self._reg_update(). self._reg_update() is a
+        # no-op if update_mask==0 (no bits to change).
+
+        # Update _REG_PA_CONFIG
+        pa_config = 0x0
+        update_mask = 0x0
+
+        # Ref DS 3.4.2 "RF Power Amplifiers"
+        if "tx_ant" in lora_cfg:
+            self._pa_boost = lora_cfg["tx_ant"].upper() == "PA_BOOST"
+            pa_boost_bit = (
+                _PA_CONFIG_PASELECT_PA_BOOST_PIN if self._pa_boost else _PA_CONFIG_PASELECT_RFO_PIN
+            )
+            pa_config |= pa_boost_bit
+            update_mask |= pa_boost_bit
+            if not self._pa_boost:
+                # When using RFO, _REG_PA_DAC can keep default value always
+                # (otherwise, it's set when output_power is set in next block)
+                self._reg_write(_REG_PA_DAC, _PA_DAC_DEFAULT_VALUE)
+
+        if "output_power" in lora_cfg:
+            # See DS 3.4.2 RF Power Amplifiers
+            dbm = int(lora_cfg["output_power"])
+            if self._pa_boost:
+                if dbm >= 20:
+                    output_power = 0x15  # 17dBm setting
+                    pa_dac = _PA_DAC_HIGH_POWER_20DBM
+                else:
+                    dbm = _clamp(dbm, 2, 17)  # +2 to +17dBm only
+                    output_power = dbm - 2
+                    pa_dac = _PA_DAC_DEFAULT_VALUE
+                self._reg_write(_REG_PA_DAC, pa_dac)
+            else:
+                # In RFO mode, Output Power is computed from two register fields
+                # - MaxPower and OutputPower.
+                #
+                # Do what the Semtech LoraMac-node driver does here, which is to
+                # set max_power at one extreme or the other (0 or 7) and then
+                # calculate the output_power setting based on this baseline.
+                dbm = _clamp(dbm, -4, 15)
+                if dbm > 0:
+                    # MaxPower to maximum
+                    pa_config |= _PA_CONFIG_MAXPOWER_MASK << _PA_CONFIG_MAXPOWER_SHIFT
+
+                    # Pout (dBm) == 10.8dBm + 0.6*maxPower - (15 - register value)
+                    # 10.8+0.6*7 == 15dBm, so pOut = register_value (0 to 15 dBm)
+                    output_power = dbm
+                else:
+                    # MaxPower field will be set to 0
+
+                    # Pout (dBm) == 10.8dBm - (15 - OutputPower)
+                    # OutputPower == Pout (dBm) + 4.2
+                    output_power = dbm + 4  # round down to 4.0, to keep using integer math
+
+            pa_config |= output_power << _PA_CONFIG_OUTPUTPOWER_SHIFT
+            update_mask |= (
+                _PA_CONFIG_OUTPUTPOWER_MASK << _PA_CONFIG_OUTPUTPOWER_SHIFT
+                | _PA_CONFIG_MAXPOWER_MASK << _PA_CONFIG_MAXPOWER_SHIFT
+            )
+
+        self._reg_update(_REG_PA_CONFIG, update_mask, pa_config)
+
+        if "pa_ramp_us" in lora_cfg:
+            # other fields in this register are reserved to 0 or unused
+            self._reg_write(
+                _REG_PA_RAMP,
+                self._get_pa_ramp_val(
+                    lora_cfg,
+                    [10, 12, 15, 20, 25, 31, 40, 50, 62, 100, 125, 250, 500, 1000, 2000, 3400],
+                ),
+            )
+
+        # If a hard reset happened then flags should be cleared already and mask should
+        # default to fully enabled, but let's be "belts and braces" sure
+        self._reg_write(_REG_IRQ_FLAGS, 0xFF)
+        self._reg_write(_REG_IRQ_FLAGS_MASK, 0)  # do IRQ masking in software for now
+
+        # Update MODEM_CONFIG1
+        modem_config1 = 0x0
+        update_mask = 0x0
+        if "bw" in lora_cfg:
+            bw = str(lora_cfg["bw"])
+            bw_reg_val, self._bw_hz = {
+                "7.8": (_MODEM_CONFIG1_BW7_8, 7800),
+                "10.4": (_MODEM_CONFIG1_BW10_4, 10400),
+                "15.6": (_MODEM_CONFIG1_BW15_6, 15600),
+                "20.8": (_MODEM_CONFIG1_BW20_8, 20800),
+                "31.25": (_MODEM_CONFIG1_BW31_25, 31250),
+                "41.7": (_MODEM_CONFIG1_BW41_7, 41700),
+                "62.5": (_MODEM_CONFIG1_BW62_5, 62500),
+                "125": (_MODEM_CONFIG1_BW125, 125000),
+                "250": (_MODEM_CONFIG1_BW250, 250000),
+                "500": (_MODEM_CONFIG1_BW500, 500000),
+            }[bw]
+            modem_config1 |= bw_reg_val << _MODEM_CONFIG1_BW_SHIFT
+            update_mask |= _MODEM_CONFIG1_BW_MASK << _MODEM_CONFIG1_BW_SHIFT
+
+        if "freq_khz" in lora_cfg or "bw" in lora_cfg:
+            # Workaround for Errata Note 2.1 "Sensitivity Optimization with a 500 kHz bandwidth"
+            if self._bw_hz == 500000 and 862_000_000 <= self._rf_freq_hz <= 1020_000_000:
+                self._reg_write(0x36, 0x02)
+                self._reg_write(0x3A, 0x64)
+            elif self._bw_hz == 500000 and 410_000_000 <= self._rf_freq_hz <= 525_000_000:
+                self._reg_write(0x36, 0x02)
+                self._reg_write(0x3A, 0x7F)
+            else:
+                # "For all other combinations of bandiwdth/frequencies, register at address 0x36
+                # should be re-set to value 0x03 and the value at address 0x3a will be
+                # automatically selected by the chip"
+                self._reg_write(0x36, 0x03)
+
+        if "coding_rate" in lora_cfg:
+            self._coding_rate = int(lora_cfg["coding_rate"])
+            if self._coding_rate < 5 or self._coding_rate > 8:
+                raise ConfigError("coding_rate")
+            # _MODEM_CONFIG1_CODING_RATE_45 == value 5 == 1
+            modem_config1 |= (self._coding_rate - 4) << _MODEM_CONFIG1_CODING_RATE_SHIFT
+            update_mask |= _MODEM_CONFIG1_CODING_RATE_MASK << _MODEM_CONFIG1_CODING_RATE_SHIFT
+
+        if "implicit_header" in lora_cfg:
+            self._implicit_header = lora_cfg["implicit_header"]
+            modem_config1 |= _flag(_MODEM_CONFIG1_IMPLICIT_HEADER_MODE_ON, self._implicit_header)
+            update_mask |= _MODEM_CONFIG1_IMPLICIT_HEADER_MODE_ON
+
+        self._reg_update(_REG_MODEM_CONFIG1, update_mask, modem_config1)
+
+        # Update MODEM_CONFIG2, for any fields that changed
+        modem_config2 = 0
+        update_mask = 0
+        if "sf" in lora_cfg:
+            sf = self._sf = int(lora_cfg["sf"])
+
+            if sf < _MODEM_CONFIG2_SF_MIN or sf > _MODEM_CONFIG2_SF_MAX:
+                raise ConfigError("sf")
+            if sf == 6 and not self._implicit_header:
+                # DS 4.1.12 "Spreading Factor"
+                raise ConfigError("SF6 requires implicit_header mode")
+
+            # Update these registers when writing 'SF'
+            self._reg_write(
+                _REG_DETECTION_THRESHOLD,
+                _DETECTION_THRESHOLD_SF6 if sf == 6 else _DETECTION_THRESHOLD_OTHER,
+            )
+            # This field has a reserved non-zero field, so do a read-modify-write update
+            self._reg_update(
+                _REG_DETECT_OPTIMIZE,
+                _DETECT_OPTIMIZE_AUTOMATIC_IF_ON | _DETECT_OPTIMIZE_MASK,
+                _DETECT_OPTIMIZE_SF6 if sf == 6 else _DETECT_OPTIMIZE_OTHER,
+            )
+
+            modem_config2 |= sf << _MODEM_CONFIG2_SF_SHIFT
+            update_mask |= _MODEM_CONFIG2_SF_MASK << _MODEM_CONFIG2_SF_SHIFT
+
+        if "crc_en" in lora_cfg:
+            self._crc_en = lora_cfg["crc_en"]
+            # I had to double-check the datasheet about this point:
+            # 1. In implicit header mode, this bit is used on both RX & TX and
+            #    should be set to get CRC generation on TX and/or checking on RX.
+            # 2. In explicit header mode, this bit is only used on TX (should CRC
+            #    be added and CRC flag set in header) and ignored on RX (CRC flag
+            #    read from header instead).
+            modem_config2 |= _flag(_MODEM_CONFIG2_RX_PAYLOAD_CRC_ON, self._crc_en)
+            update_mask |= _MODEM_CONFIG2_RX_PAYLOAD_CRC_ON
+
+        self._reg_update(_REG_MODEM_CONFIG2, update_mask, modem_config2)
+
+        # Update _REG_INVERT_IQ
+        #
+        # See comment about this register's undocumented weirdness at top of
+        # file above _REG_INVERT_IQ constant.
+        #
+        # Note also there is a second register invert_iq2 which may be set differently
+        # for transmit vs receive, see _set_invert_iq2() for that one.
+        invert_iq = 0x0
+        update_mask = 0x0
+        if "invert_iq_rx" in lora_cfg:
+            self._invert_iq[0] = lora_cfg["invert_iq_rx"]
+            invert_iq |= _flag(_INVERT_IQ_RX, lora_cfg["invert_iq_rx"])
+            update_mask |= _INVERT_IQ_RX
+        if "invert_iq_tx" in lora_cfg:
+            self._invert_iq[1] = lora_cfg["invert_iq_tx"]
+            invert_iq |= _flag(_INVERT_IQ_TX_OFF, not lora_cfg["invert_iq_tx"])  # Inverted
+            update_mask |= _INVERT_IQ_TX_OFF
+        self._reg_update(_REG_INVERT_IQ, update_mask, invert_iq)
+
+        if "preamble_len" in lora_cfg:
+            self._preamble_len = lora_cfg["preamble_len"]
+            self._reg_write(_REG_PREAMBLE_LEN_MSB, struct.pack(">H", self._preamble_len))
+
+        # Update MODEM_CONFIG3, for any fields that have changed
+        modem_config3 = 0
+        update_mask = 0
+
+        if "sf" in lora_cfg or "bw" in lora_cfg:
+            # Changing either SF or BW means the Low Data Rate Optimization may need to be changed
+            #
+            # note: BaseModem.get_n_symbols_x4() assumes this value is set automatically
+            # as follows.
+            modem_config3 |= _flag(_MODEM_CONFIG3_LOW_DATA_RATE_OPTIMIZE, self._get_ldr_en())
+            update_mask |= _MODEM_CONFIG3_LOW_DATA_RATE_OPTIMIZE
+
+        if "lna_gain" in lora_cfg:
+            lna_gain = lora_cfg["lna_gain"]
+            update_mask |= _MODEM_CONFIG3_AGC_ON
+            if lna_gain is None:  # Setting 'None' means 'Auto'
+                modem_config3 |= _MODEM_CONFIG3_AGC_ON
+            else:  # numeric register value
+                # Clear the _MODEM_CONFIG3_AGC_ON bit, and write the manual LNA gain level 1-6
+                # to the register
+                self._reg_update(
+                    _REG_LNA, _LNA_GAIN_MASK << _LNA_GAIN_SHIFT, lna_gain << _LNA_GAIN_SHIFT
+                )
+
+        if "rx_boost" in lora_cfg:
+            self._reg_update(
+                _REG_LNA,
+                _LNA_BOOST_HF_MASK << _LNA_BOOST_HF_SHIFT,
+                _flag(0x3, lora_cfg["lna_boost_hf"]),
+            )
+
+        self._reg_update(_REG_MODEM_CONFIG3, update_mask, modem_config3)
+
+        if "syncword" in lora_cfg:
+            self._reg_write(_REG_SYNC_WORD, lora_cfg["syncword"])
+
+    def _reg_write(self, reg, value):
+        self._cs(0)
+        if isinstance(value, int):
+            self._buf2[0] = reg | _WRITE_REG_BIT
+            self._buf2[1] = value
+            self._spi.write(self._buf2)
+            if _DEBUG:
+                dbg = hex(value)
+        else:  # value is a buffer
+            self._buf1[0] = reg | _WRITE_REG_BIT
+            self._spi.write(self._buf1)
+            self._spi.write(value)
+            if _DEBUG:
+                dbg = value.hex()
+        self._cs(1)
+
+        if _DEBUG:
+            print("W {:#x} ==> {}".format(reg, dbg))
+            self._reg_read(reg)  # log the readback as well
+
+    def _reg_update(self, reg, update_mask, new_value):
+        # Update register address 'reg' with byte value new_value, as masked by
+        # bit mask update_mask. Bits not set in update_mask will be kept at
+        # their pre-existing values in the register.
+        #
+        # If update_mask is zero, this function is a no-op and returns None.
+        # If update_mask is not zero, this function updates 'reg' and returns
+        # the previous complete value of 'reg' as a result.
+        #
+        # Note: this function has no way of detecting a race condition if the
+        # modem updates any bits in 'reg' that are unset in update_mask, at the
+        # same time a read/modify/write is occurring. Any such changes are
+        # overwritten with the original values.
+
+        if not update_mask:  # short-circuit if nothing to change
+            if _DEBUG:
+                # Log the current value if DEBUG is on
+                # (Note the compiler will optimize this out otherwise)
+                self._reg_read(reg)
+            return
+        old_value = self._reg_read(reg)
+        value = ((old_value & ~update_mask) & 0xFF) | (new_value & update_mask)
+        if old_value != value:
+            self._reg_write(reg, value)
+        return old_value
+
+    def _reg_read(self, reg):
+        # Read and return a single register value at address 'reg'
+        self._buf2[0] = reg
+        self._buf2[1] = 0xFF
+        self._cs(0)
+        self._spi.write_readinto(self._buf2, self._buf2)
+        self._cs(1)
+        if _DEBUG:
+            print("R {:#x} <== {:#x}".format(reg, self._buf2[1]))
+        return self._buf2[1]
+
+    def _reg_readinto(self, reg, buf):
+        # Read and return one or more register values starting at address 'reg',
+        # into buffer 'buf'.
+        self._cs(0)
+        self._spi.readinto(self._buf1, reg)
+        self._spi.readinto(buf)
+        if _DEBUG:
+            print("R {:#x} <== {}".format(reg, buf.hex()))
+        self._cs(1)
+
+    def _get_mode(self):
+        # Return the current 'Mode' field in RegOpMode
+        return self._reg_read(_REG_OPMODE) & _OPMODE_MODE_MASK
+
+    def _set_mode(self, mode, lora_en=True):
+        # Set the 'Mode' and 'LongRangeMode' fields in RegOpMode
+        # according to 'mode' and 'lora_en', respectively.
+        #
+        # If enabling or disabling LoRa mode, the radio is automatically
+        # switched into Sleep mode as required and then the requested mode is
+        # set (if not sleep mode).
+        #
+        # Returns the previous value of the RegOpMode register (unmasked).
+        mask = _OPMODE_LONGRANGEMODE_LORA | _OPMODE_MODE_MASK
+        lora_val = _flag(_OPMODE_LONGRANGEMODE_LORA, lora_en)
+        old_value = self._reg_read(_REG_OPMODE)
+        new_value = (old_value & ~mask) | lora_val | mode
+
+        if lora_val != (old_value & _OPMODE_LONGRANGEMODE_LORA):
+            # Need to switch into Sleep mode in order to change LongRangeMode flag
+            self._reg_write(_REG_OPMODE, _OPMODE_MODE_SLEEP | lora_val)
+
+        if new_value != old_value:
+            self._reg_write(_REG_OPMODE, new_value)
+
+            if _DEBUG:
+                print(
+                    "Mode {} -> {} ({:#x})".format(
+                        old_value & _OPMODE_MODE_MASK, mode, self._reg_read(_REG_OPMODE)
+                    )
+                )
+
+        return old_value
+
+    def _set_invert_iq2(self, val):
+        # Set the InvertIQ2 register on/off as needed, unless it is already set to the correct
+        # level
+        if self._invert_iq[2] == val:
+            return  # already set to the level we want
+        self._reg_write(_REG_INVERT_IQ2, _INVERT_IQ2_ON if val else _INVERT_IQ2_OFF)
+        self._invert_iq[2] = val
+
+    def _standby(self):
+        # Send the command for standby mode.
+        #
+        # **Don't call this function directly, call standby() instead.**
+        #
+        # (This private version doesn't update the driver's internal state.)
+        old_mode = self._set_mode(_OPMODE_MODE_STDBY) & _OPMODE_MODE_MASK
+        if old_mode not in (_OPMODE_MODE_STDBY, _OPMODE_MODE_SLEEP):
+            # If we just cancelled sending or receiving, clear any pending IRQs
+            self._reg_write(_REG_IRQ_FLAGS, 0xFF)
+
+    def sleep(self):
+        # Put the modem into sleep mode. Modem will wake automatically the next
+        # time host asks it for something, or call standby() to wake it manually.
+        self.standby()  # save some code size, this clears driver state for us
+        self._set_mode(_OPMODE_MODE_SLEEP)
+
+    def is_idle(self):
+        # Returns True if the modem is idle (either in standby or in sleep).
+        #
+        # Note this function can return True in the case where the modem has temporarily gone to
+        # standby, but there's a receive configured in software that will resume receiving the
+        # next time poll_recv() or poll_send() is called.
+        return self._get_mode() in (_OPMODE_MODE_STDBY, _OPMODE_MODE_SLEEP)
+
+    def calibrate_image(self):
+        # Run the modem Image & RSSI calibration process to improve receive performance.
+        #
+        # calibration will be run in the HF or LF band automatically, depending on the
+        # current radio configuration.
+        #
+        # See DS 2.1.3.8 Image and RSSI Calibration. Idea to disable TX power
+        # comes from Semtech's sx1276 driver which does this.
+
+        pa_config = self._reg_update(_REG_PA_CONFIG, 0xFF, 0)  # disable TX power
+
+        self._set_mode(_OPMODE_MODE_STDBY, False)  # Switch to FSK/OOK mode to expose RegImageCal
+
+        self._reg_update(_REG_FSKOOK_IMAGE_CAL, _IMAGE_CAL_START, _IMAGE_CAL_START)
+        while self._reg_read(_REG_FSKOOK_IMAGE_CAL) & _IMAGE_CAL_RUNNING:
+            time.sleep_ms(1)
+
+        self._set_mode(_OPMODE_MODE_STDBY)  # restore LoRA mode
+
+        self._reg_write(_REG_PA_CONFIG, pa_config)  # restore previous TX power
+
+    def calibrate(self):
+        # Run a full calibration.
+        #
+        # For SX1276, this means just the image & RSSI calibration as no other runtime
+        # calibration is implemented in the modem.
+        self.calibrate_image()
+
+    def start_recv(self, timeout_ms=None, continuous=False, rx_length=0xFF):
+        # Start receiving.
+        #
+        # Part of common low-level modem API, see README.md for usage.
+        super().start_recv(timeout_ms, continuous, rx_length)  # sets self._rx
+
+        # will_irq if DIO0 and DIO1 both hooked up, or DIO0 and no timeout
+        will_irq = self._dio0 and (self._dio1 or timeout_ms is None)
+
+        if self._tx:
+            # Send is in progress and has priority, _check_recv() will start receive
+            # once send finishes (caller needs to call poll_send() for this to happen.)
+            if _DEBUG:
+                print("Delaying receive until send completes")
+            return will_irq
+
+        # Put the modem in a known state. It's possible a different
+        # receive was in progress, this prevent anything changing while
+        # we set up the new receive
+        self._standby()  # calling private version to keep driver state as-is
+
+        # Update the InvertIQ2 setting for RX
+        self._set_invert_iq2(self._invert_iq[0])
+
+        if self._implicit_header:
+            # Payload length only needs to be set in implicit header mode
+            self._reg_write(_REG_PAYLOAD_LEN, rx_length)
+
+        if self._dio0:
+            # Field value is 0, for DIO0 = RXDone
+            update_mask = _DIO0_MAPPING_MASK << _DIO0_MAPPING_SHIFT
+            if self._dio1:
+                # Field value also 0, for DIO1 = RXTimeout
+                update_mask |= _DIO1_MAPPING_MASK << _DIO1_MAPPING_SHIFT
+            self._reg_update(_REG_DIO_MAPPING1, update_mask, 0)
+
+        if not continuous:
+            # Unlike SX1262, SX1276 doesn't have a "single RX no timeout" mode. So we set the
+            # maximum hardware timeout and resume RX in software if needed.
+            if timeout_ms is None:
+                timeout_syms = 1023
+            else:
+                t_sym_us = self._get_t_sym_us()
+                timeout_syms = (timeout_ms * 1000 + t_sym_us - 1) // t_sym_us  # round up
+
+                # if the timeout is too long for the modem, the host will
+                # automatically resume it in software. If the timeout is too
+                # short for the modem, round it silently up to the minimum
+                # timeout.
+                timeout_syms = _clamp(timeout_syms, 4, 1023)
+            self._reg_update(
+                _REG_MODEM_CONFIG2,
+                _MODEM_CONFIG2_SYMB_TIMEOUT_MSB_MASK,
+                timeout_syms >> 8,
+            )
+            self._reg_write(_REG_SYMB_TIMEOUT_LSB, timeout_syms & 0xFF)
+
+        # Allocate the full FIFO for RX
+        self._reg_write(_REG_FIFO_ADDR_PTR, 0)
+        self._reg_write(_REG_FIFO_RX_BASE_ADDR, 0)
+
+        self._set_mode(_OPMODE_MODE_RX_CONTINUOUS if continuous else _OPMODE_MODE_RX_SINGLE)
+
+        return will_irq
+
+    def _rx_flags_success(self, flags):
+        # Returns True if IRQ flags indicate successful receive.
+        # Specifically, from the bits in _IRQ_DRIVER_RX_MASK:
+        # - _IRQ_RX_DONE must be set
+        # - _IRQ_RX_TIMEOUT must not be set
+        # - _IRQ_PAYLOAD_CRC_ERROR must not be set
+        # - _IRQ_VALID_HEADER must be set if we're using explicit packet mode, ignored otherwise
+        return flags & _IRQ_DRIVER_RX_MASK == _IRQ_RX_DONE | _flag(
+            _IRQ_VALID_HEADER, not self._implicit_header
+        )
+
+    def _get_irq(self):
+        return self._reg_read(_REG_IRQ_FLAGS)
+
+    def _clear_irq(self, to_clear=0xFF):
+        return self._reg_write(_REG_IRQ_FLAGS, to_clear)
+
+    def _read_packet(self, rx_packet, flags):
+        # Private function to read received packet (RxPacket object) from the
+        # modem, if there is one.
+        #
+        # Called from poll_recv() function, which has already checked the IRQ flags
+        # and verified a valid receive happened.
+
+        ticks_ms = self._get_last_irq()  # IRQ timestamp for the receive
+
+        rx_payload_len = self._reg_read(_REG_RX_NB_BYTES)
+
+        if rx_packet is None or len(rx_packet) != rx_payload_len:
+            rx_packet = RxPacket(rx_payload_len)
+
+        self._reg_readinto(_REG_FIFO, rx_packet)
+
+        rx_packet.ticks_ms = ticks_ms
+        # units: dB*4
+        rx_packet.snr = self._reg_read(_REG_PKT_SNR_VAL)
+        if rx_packet.snr & 0x80:  # Signed 8-bit integer
+            # (avoiding using struct here to skip a heap allocation)
+            rx_packet.snr -= 0x100
+        # units: dBm
+        rx_packet.rssi = self._reg_read(_REG_PKT_RSSI_VAL) - (157 if self._pa_boost else 164)
+        rx_packet.crc_error = flags & _IRQ_PAYLOAD_CRC_ERROR != 0
+        return rx_packet
+
+    def prepare_send(self, packet):
+        # Prepare modem to start sending. Should be followed by a call to start_send()
+        #
+        # Part of common low-level modem API, see README.md for usage.
+        if len(packet) > 255:
+            raise ValueError("packet too long")
+
+        # Put the modem in a known state. Any current receive is suspended at this point,
+        # but calling _check_recv() will resume it later.
+        self._standby()  # calling private version to keep driver state as-is
+
+        if self._ant_sw:
+            self._ant_sw.tx(self._pa_boost)
+
+        self._last_irq = None
+
+        if self._dio0:
+            self._reg_update(
+                _REG_DIO_MAPPING1,
+                _DIO0_MAPPING_MASK << _DIO0_MAPPING_SHIFT,
+                1 << _DIO0_MAPPING_SHIFT,
+            )  # DIO0 = TXDone
+
+        # Update the InvertIQ2 setting for TX
+        self._set_invert_iq2(self._invert_iq[1])
+
+        # Allocate the full FIFO for TX
+        self._reg_write(_REG_FIFO_ADDR_PTR, 0)
+        self._reg_write(_REG_FIFO_TX_BASE_ADDR, 0)
+
+        self._reg_write(_REG_PAYLOAD_LEN, len(packet))
+
+        self._reg_write(_REG_FIFO, packet)
+
+        # clear the TX Done flag in case a previous call left it set
+        # (won't happen unless poll_send() was not called)
+        self._reg_write(_REG_IRQ_FLAGS, _IRQ_TX_DONE)
+
+    def start_send(self):
+        # Actually start a send that was loaded by calling prepare_send().
+        #
+        # This is split into a separate function to allow more precise timing.
+        #
+        # The driver doesn't verify the caller has done the right thing here, the
+        # modem will no doubt do something weird if prepare_send() was not called!
+        #
+        # Part of common low-level modem API, see README.md for usage.
+        self._set_mode(_OPMODE_MODE_TX)
+
+        self._tx = True
+
+        return self._dio0 is not None  # will_irq if dio0 is set
+
+    def _irq_flag_tx_done(self):
+        return _IRQ_TX_DONE
+
+
+# Define the actual modem classes that use the SyncModem & AsyncModem "mixin-like" classes
+# to create sync and async variants.
+
+try:
+    from .sync_modem import SyncModem
+
+    class SX1276(_SX127x, SyncModem):
+        pass
+
+    # Implementation note: Currently the classes SX1276, SX1277, SX1278 and
+    # SX1279 are actually all SX1276. Perhaps in the future some subclasses with
+    # software enforced limits can be added to this driver, but the differences
+    # appear very minor:
+    #
+    # - SX1276 seems like "baseline" with max freq.
+    # - SX1277 supports max SF level of 9.
+    # - SX1278 supports max freq 525MHz, therefore has no RFO_HF and RFI_HF pins.
+    # - SX1279 supports max freq 960MHz.
+    #
+    # There also appears to be no difference in silicon interface or register values to determine
+    # which model is connected.
+    SX1277 = SX1278 = SX1279 = SX1276
+
+except ImportError:
+    pass
+
+try:
+    from .async_modem import AsyncModem
+
+    class AsyncSX1276(_SX127x, AsyncModem):
+        pass
+
+    # See comment above about currently identical implementations
+    AsyncSX1277 = AsyncSX1278 = AsyncSX1279 = AsyncSX1276
+
+except ImportError:
+    pass
diff --git a/micropython/lora/lora-sx127x/manifest.py b/micropython/lora/lora-sx127x/manifest.py
new file mode 100644
index 000000000..177877091
--- /dev/null
+++ b/micropython/lora/lora-sx127x/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.2")
+require("lora")
+package("lora")
diff --git a/micropython/lora/lora-sync/lora/sync_modem.py b/micropython/lora/lora-sync/lora/sync_modem.py
new file mode 100644
index 000000000..585ae2cb4
--- /dev/null
+++ b/micropython/lora/lora-sync/lora/sync_modem.py
@@ -0,0 +1,86 @@
+# MicroPython LoRa synchronous modem driver
+# MIT license; Copyright (c) 2023 Angus Gratton
+#
+# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.
+
+import machine
+import time
+
+
+class SyncModem:
+    # Mixin-like base class that provides synchronous modem send and recv
+    # functions
+    #
+    #
+    # Don't instantiate this class directly, instantiate one of the 'AsyncXYZ'
+    # modem classes defined in the lora module.
+    #
+    # These are intended for simple applications. They block the caller until
+    # the modem operation is complete, and don't support interleaving send
+    # and receive.
+
+    def _after_init(self):
+        pass  # Needed for AsyncModem but not SyncModem
+
+    def send(self, packet, tx_at_ms=None):
+        # Send the given packet (byte sequence),
+        # and return once transmission of the packet is complete.
+        #
+        # Returns a timestamp (result of time.ticks_ms()) when the packet
+        # finished sending.
+        self.prepare_send(packet)
+
+        # If the caller specified a timestamp to start transmission at, wait until
+        # that time before triggering the send
+        if tx_at_ms is not None:
+            time.sleep_ms(max(0, time.ticks_diff(tx_at_ms, time.ticks_ms())))
+
+        will_irq = self.start_send()  # ... and go!
+
+        # sleep for the expected send time before checking if send has ended
+        time.sleep_ms(self.get_time_on_air_us(len(packet)) // 1000)
+
+        tx = True
+        while tx is True:
+            self._sync_wait(will_irq)
+            tx = self.poll_send()
+        return tx
+
+    def recv(self, timeout_ms=None, rx_length=0xFF, rx_packet=None):
+        # Attempt to a receive a single LoRa packet, timeout after timeout_ms milliseconds
+        # or wait indefinitely if no timeout is supplied (default).
+        #
+        # Returns an instance of RxPacket or None if the radio timed out while receiving.
+        #
+        # Optional rx_length argument is only used if lora_cfg["implict_header"] == True
+        # (not the default) and holds the length of the payload to receive.
+        #
+        # Optional rx_packet argument can be an existing instance of RxPacket
+        # which will be reused to save allocations, but only if the received packet
+        # is the same length as the rx_packet packet. If the length is different, a
+        # new RxPacket instance is allocated and returned.
+        will_irq = self.start_recv(timeout_ms, False, rx_length)
+        rx = True
+        while rx is True:
+            self._sync_wait(will_irq)
+            rx = self.poll_recv(rx_packet)
+        return rx or None
+
+    def _sync_wait(self, will_irq):
+        # For synchronous usage, block until an interrupt occurs or we time out
+        if will_irq:
+            for n in range(100):
+                machine.idle()
+                # machine.idle() wakes up very often, so don't actually return
+                # unless _radio_isr ran already. The outer for loop is so the
+                # modem is still polled occasionally to
+                # avoid the possibility an IRQ was lost somewhere.
+                #
+                # None of this is very efficient, power users should either use
+                # async or call the low-level API manually with better
+                # port-specific sleep configurations, in order to get the best
+                # efficiency.
+                if self.irq_triggered():
+                    break
+        else:
+            time.sleep_ms(1)
diff --git a/micropython/lora/lora-sync/manifest.py b/micropython/lora/lora-sync/manifest.py
new file mode 100644
index 000000000..1936a50e4
--- /dev/null
+++ b/micropython/lora/lora-sync/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.1")
+require("lora")
+package("lora")
diff --git a/micropython/lora/lora/lora/__init__.py b/micropython/lora/lora/lora/__init__.py
new file mode 100644
index 000000000..7f8930b8c
--- /dev/null
+++ b/micropython/lora/lora/lora/__init__.py
@@ -0,0 +1,40 @@
+# MicroPython lora module
+# MIT license; Copyright (c) 2023 Angus Gratton
+
+from .modem import RxPacket  # noqa: F401
+
+ok = False  # Flag if at least one modem driver package is installed
+
+# Various lora "sub-packages"
+
+try:
+    from .sx126x import *  # noqa: F401
+
+    ok = True
+except ImportError as e:
+    if "no module named 'lora." not in str(e):
+        raise
+
+try:
+    from .sx127x import *  # noqa: F401
+
+    ok = True
+except ImportError as e:
+    if "no module named 'lora." not in str(e):
+        raise
+
+try:
+    from .stm32wl5 import *  # noqa: F401
+
+    ok = True
+except ImportError as e:
+    if "no module named 'lora." not in str(e):
+        raise
+
+
+if not ok:
+    raise ImportError(
+        "Incomplete lora installation. Need at least one of lora-sync, lora-async and one of lora-sx126x, lora-sx127x"
+    )
+
+del ok
diff --git a/micropython/lora/lora/lora/modem.py b/micropython/lora/lora/lora/modem.py
new file mode 100644
index 000000000..499712acf
--- /dev/null
+++ b/micropython/lora/lora/lora/modem.py
@@ -0,0 +1,474 @@
+# MicroPython LoRa modem driver base class
+# MIT license; Copyright (c) 2023 Angus Gratton
+#
+# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.
+import time
+from micropython import const, schedule
+
+# Set to True to get some additional printed debug output.
+_DEBUG = const(False)
+
+
+def _clamp(v, vmin, vmax):
+    # Small utility function to clamp a value 'v' between 'vmin' and 'vmax', inclusive.
+    return min(max(vmin, v), vmax)
+
+
+def _flag(value, condition):
+    # Small utility function for returning a bit 'value' or not, based on a
+    # boolean condition. Can help make expressions to build register values more
+    # readable.
+    #
+    # Note that for value==1, can also rely on int(bool(x)) with one or both
+    # conversions being implicit, as int(True)==1 and int(False)==0
+    #
+    # There is also (condition and value) but this is (IMO) confusing to read.
+    return value if condition else 0
+
+
+class ConfigError(ValueError):
+    # Raise if there is an error in lora_cfg, saves some duplicated strings
+    def __init__(self, field):
+        super().__init__("Invalid lora_cfg {}".format(field))
+
+
+class BaseModem:
+    def __init__(self, ant_sw):
+        self._ant_sw = ant_sw
+        self._irq_callback = None
+
+        # Common configuration settings that need to be tracked by all modem drivers.
+        #
+        # Where modem hardware sets different values after reset, the driver should
+        # set them back to these defaults (if not provided by the user), so that
+        # behaviour remains consistent between different modems using the same driver.
+        self._rf_freq_hz = 0  # Needs to be set via configure()
+        self._sf = 7  # Spreading factor
+        self._bw_hz = 125000  # Reset value
+        self._coding_rate = 5
+        self._crc_en = True  # use packet CRCs
+        self._implicit_header = False  # implict vs explicit header mode
+        self._preamble_len = 12
+        self._coding_rate = 5
+
+        # CRC error counter
+        self.crc_errors = 0
+        self.rx_crc_error = False
+
+        # Current state of the modem
+
+        # _rx holds radio recv state:
+        #
+        # - False if the radio is not receiving
+        # - True if the radio is continuously receiving, or performing a single receive with
+        #   no timeout.
+        # - An int if there is a timeout set, in which case it is the is the receive deadline
+        #   (as a time.ticks_ms() timestamp).
+        #
+        # Note that self._rx can be not-False even when the radio hardware is not actually
+        # receiving, if self._tx is True (send always pauses recv.)
+        self._rx = False
+
+        # _rx_continuous is True if the modem is in continuous receive mode
+        # (this value is only valid when self._rx is also True).
+        self._rx_continuous = False
+
+        # This argument is stored from the parameter of the same name, as set in
+        # the last call to start_recv()
+        self._rx_length = None
+
+        # _tx holds radio send state and is simpler, True means sending and
+        # False means not sending.
+        self._tx = False
+
+        # timestamp (as time.ticks_ms() result) of last IRQ event
+        self._last_irq = None
+
+        # values are:
+        # - lora_cfg["invert_iq_rx"]
+        # - lora_cfg["invert_iq_tx"]
+        # - Current modem Invert setting
+        self._invert_iq = [False, False, False]
+
+        # This hook exists to allow the SyncModem & AsyncModem "mixin-like"
+        # classes to have some of their own state, without needing to manage the
+        # fuss of multiple constructor paths.
+        try:
+            self._after_init()
+        except AttributeError:
+            # If this exception happens here then one of the modem classes without a SyncModem or AsyncModem "mixin-like" class
+            # has been instantiated.
+            raise NotImplementedError(
+                "Don't instantiate this class directly, "
+                "instantiate a class from the 'lora' package"
+            )
+
+    def standby(self):
+        # Put the modem into standby. Can be used to cancel a continuous recv,
+        # or cancel a send before it completes.
+        #
+        # Calls the private function which actually sets the mode to standby, and then
+        # clears all the driver's state flags.
+        #
+        # Note this is also called before going to sleep(), to save on duplicated code.
+        self._standby()
+        self._rx = False
+        self._tx = False
+        self._last_irq = None
+        if self._ant_sw:
+            self._ant_sw.idle()
+        self._radio_isr(None)  # "soft ISR"
+
+    def _get_t_sym_us(self):
+        # Return length of a symbol in microseconds
+        return 1000_000 * (1 << self._sf) // self._bw_hz
+
+    def _get_ldr_en(self):
+        # Return true if Low Data Rate should be enabled
+        #
+        # The calculation in get_n_symbols_x4() relies on this being the same logic applied
+        # in the modem configuration routines.
+        return self._get_t_sym_us() >= 16000
+
+    def _get_pa_ramp_val(self, lora_cfg, supported):
+        # Return the PA ramp register index from the list of supported PA ramp
+        # values. If the requested ramp time is supported by the modem, round up
+        # to the next supported value.
+        #
+        # 'supported' is the list of supported ramp times, must be sorted
+        # already.
+        us = int(lora_cfg["pa_ramp_us"])
+
+        # Find the index of the lowest supported ramp time that is longer or the
+        # same value as 'us'
+        for i, v in enumerate(supported):
+            if v >= us:
+                return i
+        # The request ramp time is longer than all this modem's supported ramp times
+        raise ConfigError("pa_ramp_us")
+
+    def _symbol_offsets(self):
+        # Called from get_time_on_air_us().
+        #
+        # This function provides a way to implement the different SF5 and SF6 in SX126x,
+        # by returning two offsets: one for the overall number of symbols, and one for the
+        # number of bits used to calculate the symbol length of the payload.
+        return (0, 0)
+
+    def get_n_symbols_x4(self, payload_len):
+        # Get the number of symbols in a packet (Time-on-Air) for the current
+        # configured modem settings and the provided payload length in bytes.
+        #
+        # Result is in units of "symbols times 4" as there is a fractional term
+        # in the equation, and we want to limit ourselves to integer arithmetic.
+        #
+        # References are:
+        # - SX1261/2 DS 6.1.4 "LoRa Time-on-Air"
+        # - SX1276 DS 4.1.1 "Time on air"
+        #
+        # Note the two datasheets give the same information in different
+        # ways. SX1261/62 DS is (IMO) clearer, so this function is based on that
+        # formula. The result is equivalent to the datasheet value "Nsymbol",
+        # times 4.
+        #
+        # Note also there are unit tests for this function in tests/test_time_on_air.py,
+        # and that it's been optimised a bit for code size (with impact on readability)
+
+        # Account for a minor difference between SX126x and SX127x: they have
+        # incompatible SF 5 & 6 modes.
+        #
+        # In SX126x when using SF5 or SF6, we apply an offset of +2 symbols to
+        # the overall preamble symbol count (s_o), and an offset of -8 to the
+        # payload bit length (b_o).
+        s_o, b_o = self._symbol_offsets()
+
+        # calculate the bit length of the payload
+        #
+        # This is the part inside the max(...,0) in the datasheet
+        bits = (
+            # payload_bytes
+            8 * payload_len
+            # N_bit_crc
+            + (16 if self._crc_en else 0)
+            # (4 * SF)
+            - (4 * self._sf)
+            # +8 for most modes, except SF5/6 on SX126x where b_o == -8 so these two cancel out
+            + 8
+            + b_o
+            # N_symbol_header
+            + (0 if self._implicit_header else 20)
+        )
+        bits = max(bits, 0)
+
+        # "Bits per symbol" denominator is either (4 * SF) or (4 * (SF -2))
+        # depending on Low Data Rate Optimization
+        bps = (self._sf - (2 * self._get_ldr_en())) * 4
+
+        return (
+            # Fixed preamble portion (4.25), times 4
+            17
+            # Remainder of equation is an integer number of symbols, times 4
+            + 4
+            * (
+                # configured preamble length
+                self._preamble_len
+                +
+                # optional extra preamble symbols (4.25+2=6.25 for SX1262 SF5,SF6)
+                s_o
+                +
+                # 8 symbol constant overhead
+                8
+                +
+                # Payload symbol length
+                # (this is the term "ceil(bits / 4 * SF) * (CR + 4)" in the datasheet
+                ((bits + bps - 1) // bps) * self._coding_rate
+            )
+        )
+
+    def get_time_on_air_us(self, payload_len):
+        # Return the "Time on Air" in microseconds for a particular
+        # payload length and the current configured modem settings.
+        return self._get_t_sym_us() * self.get_n_symbols_x4(payload_len) // 4
+
+    # Modem ISR routines
+    #
+    # ISR implementation is relatively simple, just exists to signal an optional
+    # callback, record a timestamp, and wake up the hardware if
+    # needed. Application code is expected to call poll_send() or
+    # poll_recv() as applicable in order to confirm the modem state.
+    #
+    # This is a MP hard irq in some configurations.
+    def _radio_isr(self, _):
+        self._last_irq = time.ticks_ms()
+        if self._irq_callback:
+            self._irq_callback()
+        if _DEBUG:
+            print("_radio_isr")
+
+    def irq_triggered(self):
+        # Returns True if the ISR has executed since the last time a send or a receive
+        # started
+        return self._last_irq is not None
+
+    def set_irq_callback(self, callback):
+        # Set a function to be called from the radio ISR
+        #
+        # This is used by the AsyncModem implementation, but can be called in
+        # other circumstances to implement custom ISR logic.
+        #
+        # Note that callback may be called in hard ISR context.
+        self._irq_callback = callback
+
+    def _get_last_irq(self):
+        # Return the _last_irq timestamp if set by an ISR, or the
+        # current time.time_ms() timestamp otherwise.
+        if self._last_irq is None:
+            return time.ticks_ms()
+        return self._last_irq
+
+    # Common parts of receive API
+
+    def start_recv(self, timeout_ms=None, continuous=False, rx_length=0xFF):
+        # Start receiving.
+        #
+        # Part of common low-level modem API, see README.md for usage.
+        if continuous and timeout_ms is not None:
+            raise ValueError  # these two options are mutually exclusive
+
+        if timeout_ms is not None:
+            self._rx = time.ticks_add(time.ticks_ms(), timeout_ms)
+        else:
+            self._rx = True
+
+        self._rx_continuous = continuous
+        self._rx_length = rx_length
+
+        if self._ant_sw and not self._tx:
+            # this is guarded on 'not self._tx' as the subclass will not immediately
+            # start receiving if a send is in progress.
+            self._ant_sw.rx()
+
+    def poll_recv(self, rx_packet=None):
+        # Should be called while a receive is in progress:
+        #
+        # Part of common low-level modem API, see README.md for usage.
+        #
+        # This function may alter the state of the modem - it will clear
+        # RX interrupts, it may read out a packet from the FIFO, and it
+        # may resume receiving if the modem has gone to standby but receive
+        # should resume.
+
+        if self._rx is False:
+            # Not actually receiving...
+            return False
+
+        if self._tx:
+            # Actually sending, this has to complete before we
+            # resume receiving, but we'll indicate that we are still receiving.
+            #
+            # (It's not harmful to fall through here and check flags anyhow, but
+            # it is a little wasteful if an interrupt has just triggered
+            # poll_send() as well.)
+            return True
+
+        packet = None
+
+        flags = self._get_irq()
+
+        if _DEBUG and flags:
+            print("RX flags {:#x}".format(flags))
+        if flags & self._IRQ_RX_COMPLETE:
+            # There is a small potential for race conditions here in continuous
+            # RX mode. If packets are received rapidly and the call to this
+            # function delayed, then a ValidHeader interrupt (for example) might
+            # have already set for a second packet which is being received now,
+            # and clearing it will mark the second packet as invalid.
+            #
+            # However it's necessary in continuous mode as interrupt flags don't
+            # self-clear in the modem otherwise (for example, if a CRC error IRQ
+            # bit sets then it stays set on the next packet, even if that packet
+            # has a valid CRC.)
+            self._clear_irq(flags)
+            ok = self._rx_flags_success(flags)
+            if not ok:
+                # If a non-valid receive happened, increment the CRC error counter
+                self.crc_errors += 1
+            if ok or self.rx_crc_error:
+                # Successfully received a valid packet (or configured to return all packets)
+                packet = self._read_packet(rx_packet, flags)
+                if not self._rx_continuous:
+                    # Done receiving now
+                    self._end_recv()
+
+        # _check_recv() will return True if a receive is ongoing and hasn't timed out,
+        # and also manages resuming any modem receive if needed
+        #
+        # We need to always call check_recv(), but if we received a packet then this is what
+        # we should return to the caller.
+        res = self._check_recv()
+        return packet or res
+
+    def _end_recv(self):
+        # Utility function to clear the receive state
+        self._rx = False
+        if self._ant_sw:
+            self._ant_sw.idle()
+
+    def _check_recv(self):
+        # Internal function to automatically call start_recv()
+        # again if a receive has been interrupted and the host
+        # needs to start it again.
+        #
+        # Return True if modem is still receiving (or sending, but will
+        # resume receiving after send finishes).
+
+        if not self._rx:
+            return False  # Not receiving, nothing to do
+
+        if not self.is_idle():
+            return True  # Radio is already sending or receiving
+
+        rx = self._rx
+
+        timeout_ms = None
+        if isinstance(rx, int):  # timeout is set
+            timeout_ms = time.ticks_diff(rx, time.ticks_ms())
+            if timeout_ms <= 0:
+                # Timed out in software, nothing to resume
+                self._end_recv()
+                if _DEBUG:
+                    print("Timed out in software timeout_ms={}".format(timeout_ms))
+                schedule(
+                    self._radio_isr, None
+                )  # "soft irq" to unblock anything waiting on the interrupt event
+                return False
+
+        if _DEBUG:
+            print(
+                "Resuming receive timeout_ms={} continuous={} rx_length={}".format(
+                    timeout_ms, self._rx_continuous, self._rx_length
+                )
+            )
+
+        self.start_recv(timeout_ms, self._rx_continuous, self._rx_length)
+
+        # restore the previous version of _rx so ticks_ms deadline can't
+        # slowly creep forward each time this happens
+        self._rx = rx
+
+        return True
+
+    # Common parts of send API
+
+    def poll_send(self):
+        # Check the ongoing send state.
+        #
+        # Returns one of:
+        #
+        # - True if a send is ongoing and the caller
+        #   should call again.
+        # - False if no send is ongoing.
+        # - An int value exactly one time per transmission, the first time
+        #   poll_send() is called after a send ends. In this case it
+        #   is the time.ticks_ms() timestamp of the time that the send completed.
+        #
+        # Note this function only returns an int value one time (the first time it
+        # is called after send completes).
+        #
+        # Part of common low-level modem API, see README.md for usage.
+        if not self._tx:
+            return False
+
+        ticks_ms = self._get_last_irq()
+
+        if not (self._get_irq() & self._IRQ_TX_COMPLETE):
+            # Not done. If the host and modem get out
+            # of sync here, or the caller doesn't follow the sequence of
+            # send operations exactly, then can end up in a situation here
+            # where the modem has stopped sending and has gone to Standby,
+            # so _IRQ_TX_DONE is never set.
+            #
+            # For now, leaving this for the caller to do correctly. But if it becomes an issue then
+            # we can call _get_mode() here as well and check the modem is still in a TX mode.
+            return True
+
+        self._clear_irq()
+
+        self._tx = False
+
+        if self._ant_sw:
+            self._ant_sw.idle()
+
+        # The modem just finished sending, so start receiving again if needed
+        self._check_recv()
+
+        return ticks_ms
+
+
+class RxPacket(bytearray):
+    # A class to hold a packet received from a LoRa modem.
+    #
+    # The base class is bytearray, which represents the packet payload,
+    # allowing RxPacket objects to be passed anywhere that bytearrays are
+    # accepted.
+    #
+    # Some additional properties are set on the object to store metadata about
+    # the received packet.
+    def __init__(self, payload, ticks_ms=None, snr=None, rssi=None, valid_crc=True):
+        super().__init__(payload)
+        self.ticks_ms = ticks_ms
+        self.snr = snr
+        self.rssi = rssi
+        self.valid_crc = valid_crc
+
+    def __repr__(self):
+        return "{}({}, {}, {}, {}, {})".format(
+            "RxPacket",
+            repr(
+                bytes(self)
+            ),  # This is a bit wasteful, but gets us b'XYZ' rather than "bytearray(b'XYZ')"
+            self.ticks_ms,
+            self.snr,
+            self.rssi,
+            self.valid_crc,
+        )
diff --git a/micropython/lora/lora/manifest.py b/micropython/lora/lora/manifest.py
new file mode 100644
index 000000000..586c47c08
--- /dev/null
+++ b/micropython/lora/lora/manifest.py
@@ -0,0 +1,2 @@
+metadata(version="0.2.0")
+package("lora")
diff --git a/micropython/lora/tests/test_time_on_air.py b/micropython/lora/tests/test_time_on_air.py
new file mode 100644
index 000000000..56fa1ad81
--- /dev/null
+++ b/micropython/lora/tests/test_time_on_air.py
@@ -0,0 +1,310 @@
+# MicroPython LoRa modem driver time on air tests
+# MIT license; Copyright (c) 2023 Angus Gratton
+#
+# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.
+#
+# ## What is this?
+#
+# Host tests for the BaseModem.get_time_on_air_us() function. Theses against
+# dummy test values produced by the Semtech "SX1261 LoRa Calculator" software,
+# as downloaded from
+# https://lora-developers.semtech.com/documentation/product-documents/
+#
+# The app notes for SX1276 (AN1200.3) suggest a similar calculator exists for that
+# modem, but it doesn't appear to be available for download any more. I couldn't find
+# an accurate calculator for SX1276, so manually calculated the SF5 & SF6 test cases below
+# (other values should be the same as SX1262).
+#
+# ## Instructions
+#
+# These tests are intended to be run on a host PC via micropython unix port:
+#
+# cd /path/to/micropython-lib/micropython/lora
+# micropython -m tests.test_time_on_air
+#
+# Note: Using the working directory shown above is easiest way to ensure 'lora' files are imported.
+#
+from lora import SX1262, SX1276
+
+# Allow time calculations to deviate by up to this much as a ratio
+# of the expected value (due to floating point, etc.)
+TIME_ERROR_RATIO = 0.00001  # 0.001%
+
+
+def main():
+    sx1262 = SX1262(spi=DummySPI(), cs=DummyPin(), busy=DummyPin())
+    sx1276 = SX1276(spi=DummySPI(0x12), cs=DummyPin())
+
+    # Test case format is based on the layout of the Semtech Calculator UI:
+    #
+    # (modem_instance,
+    #  (modem settings),
+    #  [
+    #      ((packet config), (output values)),
+    #      ...
+    #  ],
+    # ),
+    #
+    # where each set of modem settings maps to zero or more packet config / output pairs
+    #
+    # - modem instance is sx1262 or sx1276 (SF5 & SF6 are different between these modems)
+    # - (modem settings) is (sf, bw (in khz), coding_rate, low_datarate_optimize)
+    # - (packet config) is (preamble_len, payload_len, explicit_header, crc_en)
+    # - (output values) is (total_symbols_excl, symbol_time in ms, time_on_air in ms)
+    #
+    # NOTE: total_symbols_excl is the value shown in the calculator output,
+    # which doesn't include 8 symbols of constant overhead between preamble and
+    # header+payload+crc. I think this is a bug in the Semtech calculator(!).
+    # These 8 symbols are included when the calculator derives the total time on
+    # air.
+    #
+    # NOTE ALSO: The "symbol_time" only depends on the modem settings so is
+    # repeated each group of test cases, and the "time_on_air" is the previous
+    # two output values multiplied (after accounting for the 8 symbols noted
+    # above). This repetition is deliberate to make the cases easier to read
+    # line-by-line when comparing to the calculator window.
+    CASES = [
+        (
+            sx1262,
+            (12, 500, 5, False),  # Calculator defaults when launching calculator
+            [
+                ((8, 1, True, True), (17.25, 8.192, 206.848)),  # Calculator defaults
+                ((12, 64, True, True), (71.25, 8.192, 649.216)),
+                ((8, 1, True, False), (12.25, 8.192, 165.888)),
+                ((8, 192, True, True), (172.25, 8.192, 1476.608)),
+                ((12, 16, False, False), (26.25, 8.192, 280.576)),
+            ],
+        ),
+        (
+            sx1262,
+            (8, 125, 6, False),
+            [
+                ((8, 1, True, True), (18.25, 2.048, 53.760)),
+                ((8, 2, True, True), (18.25, 2.048, 53.760)),
+                ((8, 2, True, False), (18.25, 2.048, 53.760)),
+                ((8, 3, True, True), (24.25, 2.048, 66.048)),
+                ((8, 3, True, False), (18.25, 2.048, 53.760)),
+                ((8, 4, True, True), (24.25, 2.048, 66.048)),
+                ((8, 4, True, False), (18.25, 2.048, 53.760)),
+                ((8, 5, True, True), (24.25, 2.048, 66.048)),
+                ((8, 5, True, False), (24.25, 2.048, 66.048)),
+                ((8, 253, True, True), (396.25, 2.048, 827.904)),
+                ((8, 253, True, False), (396.25, 2.048, 827.904)),
+                ((12, 5, False, True), (22.25, 2.048, 61.952)),
+                ((12, 5, False, False), (22.25, 2.048, 61.952)),
+                ((12, 10, False, True), (34.25, 2.048, 86.528)),
+                ((12, 253, False, True), (394.25, 2.048, 823.808)),
+            ],
+        ),
+        # quick check that sx1276 is the same as sx1262 for SF>6
+        (
+            sx1276,
+            (8, 125, 6, False),
+            [
+                ((8, 1, True, True), (18.25, 2.048, 53.760)),
+                ((8, 2, True, True), (18.25, 2.048, 53.760)),
+                ((12, 5, False, True), (22.25, 2.048, 61.952)),
+                ((12, 5, False, False), (22.25, 2.048, 61.952)),
+            ],
+        ),
+        # SF5 on SX1262
+        (
+            sx1262,
+            (5, 500, 5, False),
+            [
+                (
+                    (2, 1, True, False),
+                    (13.25, 0.064, 1.360),
+                ),  # Shortest possible LoRa packet?
+                ((2, 1, True, True), (18.25, 0.064, 1.680)),
+                ((12, 1, False, False), (18.25, 0.064, 1.680)),
+                ((12, 253, False, True), (523.25, 0.064, 34.000)),
+            ],
+        ),
+        (
+            sx1262,
+            (5, 125, 8, False),
+            [
+                ((12, 253, False, True), (826.25, 0.256, 213.568)),
+            ],
+        ),
+        # SF5 on SX1276
+        #
+        # Note: SF5 & SF6 settings are different between SX1262 & SX1276.
+        #
+        # There's no Semtech official calculator available for SX1276, so the
+        # symbol length is calculated by copying the formula from the datasheet
+        # "Time on air" section. Symbol time is the same as SX1262. Then the
+        # time on air is manually calculated by multiplying the two together.
+        #
+        # see the functions sx1276_num_payload and sx1276_num_symbols at end of this module
+        # for the actual functions used.
+        (
+            sx1276,
+            (5, 500, 5, False),
+            [
+                (
+                    (2, 1, True, False),
+                    (19.25 - 8, 0.064, 1.232),
+                ),  # Shortest possible LoRa packet?
+                ((2, 1, True, True), (24.25 - 8, 0.064, 1.552)),
+                ((12, 1, False, False), (24.25 - 8, 0.064, 1.552)),
+                ((12, 253, False, True), (534.25 - 8, 0.064, 34.192)),
+            ],
+        ),
+        (
+            sx1276,
+            (5, 125, 8, False),
+            [
+                ((12, 253, False, True), (840.25 - 8, 0.256, 215.104)),
+            ],
+        ),
+        (
+            sx1262,
+            (12, 7.81, 8, True),  # Slowest possible
+            [
+                ((128, 253, True, True), (540.25, 524.456, 287532.907)),
+                ((1000, 253, True, True), (1412.25, 524.456, 744858.387)),
+            ],
+        ),
+        (
+            sx1262,
+            (11, 10.42, 7, True),
+            [
+                ((25, 16, True, True), (57.25, 196.545, 12824.568)),
+                ((25, 16, False, False), (50.25, 196.545, 11448.752)),
+            ],
+        ),
+    ]
+
+    tests = 0
+    failures = set()
+    for modem, modem_settings, packets in CASES:
+        (sf, bw_khz, coding_rate, low_datarate_optimize) = modem_settings
+        print(
+            f"Modem config sf={sf} bw={bw_khz}kHz coding_rate=4/{coding_rate} "
+            + f"low_datarate_optimize={low_datarate_optimize}"
+        )
+
+        # We don't call configure() as the Dummy interfaces won't handle it,
+        # just update the BaseModem fields directly
+        modem._sf = sf
+        modem._bw_hz = int(bw_khz * 1000)
+        modem._coding_rate = coding_rate
+
+        # Low datarate optimize on/off is auto-configured in the current driver,
+        # check the automatic selection matches the test case from the
+        # calculator
+        if modem._get_ldr_en() != low_datarate_optimize:
+            print(
+                f" -- ERROR: Test case has low_datarate_optimize={low_datarate_optimize} "
+                + f"but modem selects {modem._get_ldr_en()}"
+            )
+            failures += 1
+            continue  # results will not match so don't run any of the packet test cases
+
+        for packet_config, expected_outputs in packets:
+            preamble_len, payload_len, explicit_header, crc_en = packet_config
+            print(
+                f" -- preamble_len={preamble_len} payload_len={payload_len} "
+                + f"explicit_header={explicit_header} crc_en={crc_en}"
+            )
+            modem._preamble_len = preamble_len
+            modem._implicit_header = not explicit_header  # opposite logic to calculator
+            modem._crc_en = crc_en
+
+            # Now calculate the symbol length and times and compare with the expected valuesd
+            (
+                expected_symbols,
+                expected_symbol_time,
+                expected_time_on_air,
+            ) = expected_outputs
+
+            print(f" ---- calculator shows total length {expected_symbols}")
+            expected_symbols += 8  # Account for the calculator bug mentioned in the comment above
+
+            n_symbols = modem.get_n_symbols_x4(payload_len) / 4.0
+            symbol_time_us = modem._get_t_sym_us()
+            time_on_air_us = modem.get_time_on_air_us(payload_len)
+
+            tests += 1
+
+            if n_symbols == expected_symbols:
+                print(f" ---- symbols {n_symbols}")
+            else:
+                print(f" ---- SYMBOL COUNT ERROR expected {expected_symbols} got {n_symbols}")
+                failures.add((modem, modem_settings, packet_config))
+
+            max_error = expected_symbol_time * 1000 * TIME_ERROR_RATIO
+            if abs(int(expected_symbol_time * 1000) - symbol_time_us) <= max_error:
+                print(f" ---- symbol time {expected_symbol_time}ms")
+            else:
+                print(
+                    f" ---- SYMBOL TIME ERROR expected {expected_symbol_time}ms "
+                    + f"got {symbol_time_us}us"
+                )
+                failures.add((modem, modem_settings, packet_config))
+
+            max_error = expected_time_on_air * 1000 * TIME_ERROR_RATIO
+            if abs(int(expected_time_on_air * 1000) - time_on_air_us) <= max_error:
+                print(f" ---- time on air {expected_time_on_air}ms")
+            else:
+                print(
+                    f" ---- TIME ON AIR ERROR expected {expected_time_on_air}ms "
+                    + f"got {time_on_air_us}us"
+                )
+                failures.add((modem, modem_settings, packet_config))
+
+        print("************************")
+
+    print(f"\n{len(failures)}/{tests} tests failed")
+    if failures:
+        print("FAILURES:")
+        for f in failures:
+            print(f)
+        raise SystemExit(1)
+    print("SUCCESS")
+
+
+class DummySPI:
+    # Dummy SPI Interface allows us to use normal constructors
+    #
+    # Reading will always return the 'always_read' value
+    def __init__(self, always_read=0x00):
+        self.always_read = always_read
+
+    def write_readinto(self, _wrbuf, rdbuf):
+        for i in range(len(rdbuf)):
+            rdbuf[i] = self.always_read
+
+
+class DummyPin:
+    # Dummy Pin interface allows us to use normal constructors
+    def __init__(self):
+        pass
+
+    def __call__(self, _=None):
+        pass
+
+
+# Copies of the functions used to calculate SX1276 SF5, SF6 test case symbol counts.
+# (see comments above).
+#
+# These are written as closely to the SX1276 datasheet "Time on air" section as
+# possible, quite different from the BaseModem implementation.
+
+
+def sx1276_n_payload(pl, sf, ih, de, cr, crc):
+    import math
+
+    ceil_arg = 8 * pl - 4 * sf + 28 + 16 * crc - 20 * ih
+    ceil_arg /= 4 * (sf - 2 * de)
+    return 8 + max(math.ceil(ceil_arg) * (cr + 4), 0)
+
+
+def sx1276_n_syms(pl, sf, ih, de, cr, crc, n_preamble):
+    return sx1276_n_payload(pl, sf, ih, de, cr, crc) + n_preamble + 4.25
+
+
+if __name__ == "__main__":
+    main()
diff --git a/micropython/mip-cmdline/manifest.py b/micropython/mip-cmdline/manifest.py
new file mode 100644
index 000000000..cf8e4b4f2
--- /dev/null
+++ b/micropython/mip-cmdline/manifest.py
@@ -0,0 +1,6 @@
+metadata(version="0.1.0", description="Optional support for running `micropython -m mip`")
+
+require("argparse")
+require("mip")
+
+package("mip")
diff --git a/micropython/mip-cmdline/mip/__main__.py b/micropython/mip-cmdline/mip/__main__.py
new file mode 100644
index 000000000..7732638b2
--- /dev/null
+++ b/micropython/mip-cmdline/mip/__main__.py
@@ -0,0 +1,46 @@
+# MicroPython package installer command line
+# MIT license; Copyright (c) 2022 Jim Mussared
+
+import argparse
+import sys
+
+
+def do_install():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-t",
+        "--target",
+        help="Directory to start discovery",
+    )
+    parser.add_argument(
+        "-i",
+        "--index",
+        help="Pattern to match test files",
+    )
+    parser.add_argument(
+        "--mpy",
+        action="store_true",
+        help="download as compiled .mpy files (default)",
+    )
+    parser.add_argument(
+        "--no-mpy",
+        action="store_true",
+        help="download as .py source files",
+    )
+    parser.add_argument("package", nargs="+")
+    args = parser.parse_args(args=sys.argv[2:])
+
+    from . import install
+
+    for package in args.package:
+        version = None
+        if "@" in package:
+            package, version = package.split("@")
+        install(package, args.index, args.target, version, not args.no_mpy)
+
+
+if len(sys.argv) >= 2:
+    if sys.argv[1] == "install":
+        do_install()
+    else:
+        print('mip: Unknown command "{}"'.format(sys.argv[1]))
diff --git a/micropython/mip/manifest.py b/micropython/mip/manifest.py
new file mode 100644
index 000000000..9fb94ebcb
--- /dev/null
+++ b/micropython/mip/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.4.1", description="On-device package installer for network-capable boards")
+
+require("requests")
+
+package("mip", opt=3)
diff --git a/micropython/mip/mip/__init__.py b/micropython/mip/mip/__init__.py
new file mode 100644
index 000000000..7c0fb4d3a
--- /dev/null
+++ b/micropython/mip/mip/__init__.py
@@ -0,0 +1,187 @@
+# MicroPython package installer
+# MIT license; Copyright (c) 2022 Jim Mussared
+
+from micropython import const
+import requests
+import sys
+
+
+_PACKAGE_INDEX = const("https://micropython.org/pi/v2")
+_CHUNK_SIZE = 128
+
+allowed_mip_url_prefixes = ("http://", "https://", "github:", "gitlab:")
+
+
+# This implements os.makedirs(os.dirname(path))
+def _ensure_path_exists(path):
+    import os
+
+    split = path.split("/")
+
+    # Handle paths starting with "/".
+    if not split[0]:
+        split.pop(0)
+        split[0] = "/" + split[0]
+
+    prefix = ""
+    for i in range(len(split) - 1):
+        prefix += split[i]
+        try:
+            os.stat(prefix)
+        except:
+            os.mkdir(prefix)
+        prefix += "/"
+
+
+# Copy from src (stream) to dest (function-taking-bytes)
+def _chunk(src, dest):
+    buf = memoryview(bytearray(_CHUNK_SIZE))
+    while True:
+        n = src.readinto(buf)
+        if n == 0:
+            break
+        dest(buf if n == _CHUNK_SIZE else buf[:n])
+
+
+# Check if the specified path exists and matches the hash.
+def _check_exists(path, short_hash):
+    import os
+
+    try:
+        import binascii
+        import hashlib
+
+        with open(path, "rb") as f:
+            hs256 = hashlib.sha256()
+            _chunk(f, hs256.update)
+            existing_hash = str(binascii.hexlify(hs256.digest())[: len(short_hash)], "utf-8")
+            return existing_hash == short_hash
+    except:
+        return False
+
+
+def _rewrite_url(url, branch=None):
+    if not branch:
+        branch = "HEAD"
+    if url.startswith("github:"):
+        url = url[7:].split("/")
+        url = (
+            "https://raw.githubusercontent.com/"
+            + url[0]
+            + "/"
+            + url[1]
+            + "/"
+            + branch
+            + "/"
+            + "/".join(url[2:])
+        )
+    elif url.startswith("gitlab:"):
+        url = url[7:].split("/")
+        url = (
+            "https://gitlab.com/"
+            + url[0]
+            + "/"
+            + url[1]
+            + "/-/raw/"
+            + branch
+            + "/"
+            + "/".join(url[2:])
+        )
+    return url
+
+
+def _download_file(url, dest):
+    response = requests.get(url)
+    try:
+        if response.status_code != 200:
+            print("Error", response.status_code, "requesting", url)
+            return False
+
+        print("Copying:", dest)
+        _ensure_path_exists(dest)
+        with open(dest, "wb") as f:
+            _chunk(response.raw, f.write)
+
+        return True
+    finally:
+        response.close()
+
+
+def _install_json(package_json_url, index, target, version, mpy):
+    response = requests.get(_rewrite_url(package_json_url, version))
+    try:
+        if response.status_code != 200:
+            print("Package not found:", package_json_url)
+            return False
+
+        package_json = response.json()
+    finally:
+        response.close()
+    for target_path, short_hash in package_json.get("hashes", ()):
+        fs_target_path = target + "/" + target_path
+        if _check_exists(fs_target_path, short_hash):
+            print("Exists:", fs_target_path)
+        else:
+            file_url = "{}/file/{}/{}".format(index, short_hash[:2], short_hash)
+            if not _download_file(file_url, fs_target_path):
+                print("File not found: {} {}".format(target_path, short_hash))
+                return False
+    base_url = package_json_url.rpartition("/")[0]
+    for target_path, url in package_json.get("urls", ()):
+        fs_target_path = target + "/" + target_path
+        is_full_url = any(url.startswith(p) for p in allowed_mip_url_prefixes)
+        if base_url and not is_full_url:
+            url = f"{base_url}/{url}"  # Relative URLs
+        if not _download_file(_rewrite_url(url, version), fs_target_path):
+            print("File not found: {} {}".format(target_path, url))
+            return False
+    for dep, dep_version in package_json.get("deps", ()):
+        if not _install_package(dep, index, target, dep_version, mpy):
+            return False
+    return True
+
+
+def _install_package(package, index, target, version, mpy):
+    if any(package.startswith(p) for p in allowed_mip_url_prefixes):
+        if package.endswith(".py") or package.endswith(".mpy"):
+            print("Downloading {} to {}".format(package, target))
+            return _download_file(
+                _rewrite_url(package, version), target + "/" + package.rsplit("/")[-1]
+            )
+        else:
+            if not package.endswith(".json"):
+                if not package.endswith("/"):
+                    package += "/"
+                package += "package.json"
+            print("Installing {} to {}".format(package, target))
+    else:
+        if not version:
+            version = "latest"
+        print("Installing {} ({}) from {} to {}".format(package, version, index, target))
+
+        mpy_version = (
+            sys.implementation._mpy & 0xFF if mpy and hasattr(sys.implementation, "_mpy") else "py"
+        )
+
+        package = "{}/package/{}/{}/{}.json".format(index, mpy_version, package, version)
+
+    return _install_json(package, index, target, version, mpy)
+
+
+def install(package, index=None, target=None, version=None, mpy=True):
+    if not target:
+        for p in sys.path:
+            if not p.startswith("/rom") and p.endswith("/lib"):
+                target = p
+                break
+        else:
+            print("Unable to find lib dir in sys.path")
+            return
+
+    if not index:
+        index = _PACKAGE_INDEX
+
+    if _install_package(package, index.rstrip("/"), target, version, mpy):
+        print("Done")
+    else:
+        print("Package may be partially installed")
diff --git a/micropython/net/ntptime/manifest.py b/micropython/net/ntptime/manifest.py
new file mode 100644
index 000000000..15f832966
--- /dev/null
+++ b/micropython/net/ntptime/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="NTP client.", version="0.1.1")
+
+module("ntptime.py", opt=3)
diff --git a/micropython/net/ntptime/ntptime.py b/micropython/net/ntptime/ntptime.py
new file mode 100644
index 000000000..d77214d1d
--- /dev/null
+++ b/micropython/net/ntptime/ntptime.py
@@ -0,0 +1,68 @@
+from time import gmtime
+import socket
+import struct
+
+# The NTP host can be configured at runtime by doing: ntptime.host = 'myhost.org'
+host = "pool.ntp.org"
+# The NTP socket timeout can be configured at runtime by doing: ntptime.timeout = 2
+timeout = 1
+
+
+def time():
+    NTP_QUERY = bytearray(48)
+    NTP_QUERY[0] = 0x1B
+    addr = socket.getaddrinfo(host, 123)[0][-1]
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    try:
+        s.settimeout(timeout)
+        s.sendto(NTP_QUERY, addr)
+        msg = s.recv(48)
+    finally:
+        s.close()
+    val = struct.unpack("!I", msg[40:44])[0]
+
+    # 2024-01-01 00:00:00 converted to an NTP timestamp
+    MIN_NTP_TIMESTAMP = 3913056000
+
+    # Y2036 fix
+    #
+    # The NTP timestamp has a 32-bit count of seconds, which will wrap back
+    # to zero on 7 Feb 2036 at 06:28:16.
+    #
+    # We know that this software was written during 2024 (or later).
+    # So we know that timestamps less than MIN_NTP_TIMESTAMP are impossible.
+    # So if the timestamp is less than MIN_NTP_TIMESTAMP, that probably means
+    # that the NTP time wrapped at 2^32 seconds.  (Or someone set the wrong
+    # time on their NTP server, but we can't really do anything about that).
+    #
+    # So in that case, we need to add in those extra 2^32 seconds, to get the
+    # correct timestamp.
+    #
+    # This means that this code will work until the year 2160.  More precisely,
+    # this code will not work after 7th Feb 2160 at 06:28:15.
+    #
+    if val < MIN_NTP_TIMESTAMP:
+        val += 0x100000000
+
+    # Convert timestamp from NTP format to our internal format
+
+    EPOCH_YEAR = gmtime(0)[0]
+    if EPOCH_YEAR == 2000:
+        # (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60
+        NTP_DELTA = 3155673600
+    elif EPOCH_YEAR == 1970:
+        # (date(1970, 1, 1) - date(1900, 1, 1)).days * 24*60*60
+        NTP_DELTA = 2208988800
+    else:
+        raise Exception("Unsupported epoch: {}".format(EPOCH_YEAR))
+
+    return val - NTP_DELTA
+
+
+# There's currently no timezone support in MicroPython, and the RTC is set in UTC time.
+def settime():
+    t = time()
+    import machine
+
+    tm = gmtime(t)
+    machine.RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))
diff --git a/micropython/net/webrepl/manifest.py b/micropython/net/webrepl/manifest.py
new file mode 100644
index 000000000..20527db4f
--- /dev/null
+++ b/micropython/net/webrepl/manifest.py
@@ -0,0 +1,4 @@
+metadata(description="WebREPL server.", version="0.1.0")
+
+module("webrepl.py", opt=3)
+module("webrepl_setup.py", opt=3)
diff --git a/micropython/net/webrepl/webrepl.py b/micropython/net/webrepl/webrepl.py
new file mode 100644
index 000000000..00da8155c
--- /dev/null
+++ b/micropython/net/webrepl/webrepl.py
@@ -0,0 +1,178 @@
+# This module should be imported from REPL, not run from command line.
+import binascii
+import hashlib
+from micropython import const
+import network
+import os
+import socket
+import sys
+import websocket
+import _webrepl
+
+listen_s = None
+client_s = None
+
+DEBUG = 0
+
+_DEFAULT_STATIC_HOST = const("https://micropython.org/webrepl/")
+static_host = _DEFAULT_STATIC_HOST
+
+
+def server_handshake(cl):
+    req = cl.makefile("rwb", 0)
+    # Skip HTTP GET line.
+    l = req.readline()
+    if DEBUG:
+        sys.stdout.write(repr(l))
+
+    webkey = None
+    upgrade = False
+    websocket = False
+
+    while True:
+        l = req.readline()
+        if not l:
+            # EOF in headers.
+            return False
+        if l == b"\r\n":
+            break
+        if DEBUG:
+            sys.stdout.write(l)
+        h, v = [x.strip() for x in l.split(b":", 1)]
+        if DEBUG:
+            print((h, v))
+        if h == b"Sec-WebSocket-Key":
+            webkey = v
+        elif h == b"Connection" and b"Upgrade" in v:
+            upgrade = True
+        elif h == b"Upgrade" and v == b"websocket":
+            websocket = True
+
+    if not (upgrade and websocket and webkey):
+        return False
+
+    if DEBUG:
+        print("Sec-WebSocket-Key:", webkey, len(webkey))
+
+    d = hashlib.sha1(webkey)
+    d.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
+    respkey = d.digest()
+    respkey = binascii.b2a_base64(respkey)[:-1]
+    if DEBUG:
+        print("respkey:", respkey)
+
+    cl.send(
+        b"""\
+HTTP/1.1 101 Switching Protocols\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Accept: """
+    )
+    cl.send(respkey)
+    cl.send("\r\n\r\n")
+
+    return True
+
+
+def send_html(cl):
+    cl.send(
+        b"""\
+HTTP/1.0 200 OK\r
+\r
+<base href=\""""
+    )
+    cl.send(static_host)
+    cl.send(
+        b"""\"></base>\r
+<script src="webrepl_content.js"></script>\r
+"""
+    )
+    cl.close()
+
+
+def setup_conn(port, accept_handler):
+    global listen_s
+    listen_s = socket.socket()
+    listen_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+    ai = socket.getaddrinfo("0.0.0.0", port)
+    addr = ai[0][4]
+
+    listen_s.bind(addr)
+    listen_s.listen(1)
+    if accept_handler:
+        listen_s.setsockopt(socket.SOL_SOCKET, 20, accept_handler)
+    for i in (network.WLAN.IF_AP, network.WLAN.IF_STA):
+        iface = network.WLAN(i)
+        if iface.active():
+            print("WebREPL server started on http://%s:%d/" % (iface.ifconfig()[0], port))
+    return listen_s
+
+
+def accept_conn(listen_sock):
+    global client_s
+    cl, remote_addr = listen_sock.accept()
+
+    if not server_handshake(cl):
+        send_html(cl)
+        return False
+
+    prev = os.dupterm(None)
+    os.dupterm(prev)
+    if prev:
+        print("\nConcurrent WebREPL connection from", remote_addr, "rejected")
+        cl.close()
+        return False
+    print("\nWebREPL connection from:", remote_addr)
+    client_s = cl
+
+    ws = websocket.websocket(cl, True)
+    ws = _webrepl._webrepl(ws)
+    cl.setblocking(False)
+    # notify REPL on socket incoming data (ESP32/ESP8266-only)
+    if hasattr(os, "dupterm_notify"):
+        cl.setsockopt(socket.SOL_SOCKET, 20, os.dupterm_notify)
+    os.dupterm(ws)
+
+    return True
+
+
+def stop():
+    global listen_s, client_s
+    os.dupterm(None)
+    if client_s:
+        client_s.close()
+    if listen_s:
+        listen_s.close()
+
+
+def start(port=8266, password=None, accept_handler=accept_conn):
+    global static_host
+    stop()
+    webrepl_pass = password
+    if webrepl_pass is None:
+        try:
+            import webrepl_cfg
+
+            webrepl_pass = webrepl_cfg.PASS
+            if hasattr(webrepl_cfg, "BASE"):
+                static_host = webrepl_cfg.BASE
+        except:
+            print("WebREPL is not configured, run 'import webrepl_setup'")
+
+    _webrepl.password(webrepl_pass)
+    s = setup_conn(port, accept_handler)
+
+    if accept_handler is None:
+        print("Starting webrepl in foreground mode")
+        # Run accept_conn to serve HTML until we get a websocket connection.
+        while not accept_conn(s):
+            pass
+    elif password is None:
+        print("Started webrepl in normal mode")
+    else:
+        print("Started webrepl in manual override mode")
+
+
+def start_foreground(port=8266, password=None):
+    start(port, password, None)
diff --git a/micropython/net/webrepl/webrepl_setup.py b/micropython/net/webrepl/webrepl_setup.py
new file mode 100644
index 000000000..16e5f76e6
--- /dev/null
+++ b/micropython/net/webrepl/webrepl_setup.py
@@ -0,0 +1,107 @@
+import sys
+
+import os
+import machine
+
+RC = "./boot.py"
+CONFIG = "./webrepl_cfg.py"
+
+
+def input_choice(prompt, choices):
+    while 1:
+        resp = input(prompt)
+        if resp in choices:
+            return resp
+
+
+def getpass(prompt):
+    return input(prompt)
+
+
+def input_pass():
+    while 1:
+        passwd1 = getpass("New password (4-9 chars): ")
+        if len(passwd1) < 4 or len(passwd1) > 9:
+            print("Invalid password length")
+            continue
+        passwd2 = getpass("Confirm password: ")
+        if passwd1 == passwd2:
+            return passwd1
+        print("Passwords do not match")
+
+
+def exists(fname):
+    try:
+        with open(fname):
+            pass
+        return True
+    except OSError:
+        return False
+
+
+def get_daemon_status():
+    with open(RC) as f:
+        for l in f:
+            if "webrepl" in l:
+                if l.startswith("#"):
+                    return False
+                return True
+        return None
+
+
+def change_daemon(action):
+    LINES = ("import webrepl", "webrepl.start()")
+    with open(RC) as old_f, open(RC + ".tmp", "w") as new_f:
+        found = False
+        for l in old_f:
+            for patt in LINES:
+                if patt in l:
+                    found = True
+                    if action and l.startswith("#"):
+                        l = l[1:]
+                    elif not action and not l.startswith("#"):
+                        l = "#" + l
+            new_f.write(l)
+        if not found:
+            new_f.write("import webrepl\nwebrepl.start()\n")
+    # FatFs rename() is not POSIX compliant, will raise OSError if
+    # dest file exists.
+    os.remove(RC)
+    os.rename(RC + ".tmp", RC)
+
+
+def main():
+    status = get_daemon_status()
+
+    print("WebREPL daemon auto-start status:", "enabled" if status else "disabled")
+    print("\nWould you like to (E)nable or (D)isable it running on boot?")
+    print("(Empty line to quit)")
+    resp = input("> ").upper()
+
+    if resp == "E":
+        if exists(CONFIG):
+            resp2 = input_choice(
+                "Would you like to change WebREPL password? (y/n) ", ("y", "n", "")
+            )
+        else:
+            print("To enable WebREPL, you must set password for it")
+            resp2 = "y"
+
+        if resp2 == "y":
+            passwd = input_pass()
+            with open(CONFIG, "w") as f:
+                f.write("PASS = %r\n" % passwd)
+
+    if resp not in ("D", "E") or (resp == "D" and not status) or (resp == "E" and status):
+        print("No further action required")
+        sys.exit()
+
+    change_daemon(resp == "E")
+
+    print("Changes will be activated after reboot")
+    resp = input_choice("Would you like to reboot now? (y/n) ", ("y", "n", ""))
+    if resp == "y":
+        machine.reset()
+
+
+main()
diff --git a/micropython/senml/README.md b/micropython/senml/README.md
new file mode 100644
index 000000000..9b79cbf67
--- /dev/null
+++ b/micropython/senml/README.md
@@ -0,0 +1,12 @@
+# Introduction
+
+The SenML library helps you create and parse [senml documents](https://tools.ietf.org/html/draft-ietf-core-senml-13)
+in both json and cbor format.
+
+# key features
+
+- Object oriented design.
+- built in support for [senml's unit registry](https://tools.ietf.org/html/draft-ietf-core-senml-12#section-12.1)
+- extensible for new data types
+- direct support to read/write in json and cbor format.
+- automatically adjusts record data with respect to base time, base value & base sum.
diff --git a/micropython/senml/docs/_config.yml b/micropython/senml/docs/_config.yml
new file mode 100644
index 000000000..c74188174
--- /dev/null
+++ b/micropython/senml/docs/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-slate
\ No newline at end of file
diff --git a/micropython/senml/docs/index.md b/micropython/senml/docs/index.md
new file mode 100644
index 000000000..91ed7fe99
--- /dev/null
+++ b/micropython/senml/docs/index.md
@@ -0,0 +1,13 @@
+Welcome to the API documet site for the micro-python SenML library.
+
+The following api sections are available:
+
+- [senml-base](./senml_base): the base class for all senml objects.
+- [senml-pack](./senml_pack): the class that represents root documents.
+- [senml-record](./senml_record): the class that stores sensor measurements
+- [senml-unit](./senml_unit): the list of all unit names that can be used.
+
+
+
+Copyright (c) 2018 KPN
+Copyright (c) 2023 MicroPython
diff --git a/micropython/senml/docs/senml_base.md b/micropython/senml/docs/senml_base.md
new file mode 100644
index 000000000..feeff22e2
--- /dev/null
+++ b/micropython/senml/docs/senml_base.md
@@ -0,0 +1,8 @@
+
+# senml_base Module
+
+
+## senml_base.SenmlBase Objects
+
+
+the base class for all senml objects. 
diff --git a/micropython/senml/docs/senml_pack.md b/micropython/senml/docs/senml_pack.md
new file mode 100644
index 000000000..4a51cff78
--- /dev/null
+++ b/micropython/senml/docs/senml_pack.md
@@ -0,0 +1,216 @@
+
+# senml_pack Module
+
+
+## senml_pack.SenmlPack Objects
+
+
+represents a senml pack object. This can contain multiple records but also other (child) pack objects.
+When the pack object only contains records, it represents the data of a device.
+If the pack object has child pack objects, then it represents a gateway 
+
+### __enter__ 
+
+```Python
+__enter__(self)
+``` 
+
+for supporting the 'with' statement
+
+
+_returns_: self 
+
+### __exit__ 
+
+```Python
+__exit__(self, exc_type, exc_val, exc_tb)
+``` 
+
+when destroyed in a 'with' statement, make certain that the item is removed from the parent list.
+
+
+_returns_: None 
+
+### __init__ 
+
+```Python
+__init__(self, name, callback=None)
+``` 
+
+initialize the object
+
+_parameters:_
+
+- `name:` {string} the name of the pack 
+
+### __iter__ 
+
+```Python
+__iter__(self)
+``` 
+
+
+
+### add 
+
+```Python
+adds the item to the list of records
+``` 
+
+
+_parameters:_
+
+- `item:` {SenmlRecord} the item that needs to be added to the pack
+
+
+_returns_: None 
+
+### base_sum 
+
+the base sum of the pack.
+
+
+_returns_: a number 
+
+### base_time 
+
+Get the base time assigned to this pack object.
+While rendering, this value will be subtracted from the value of the records.
+
+
+_returns_: unix time stamp representing the base time 
+
+### base_value 
+
+the base value of the pack. The value of the records will be subtracted by this value during rendering.
+While parsing, this value is added to the value of the records.
+
+
+_returns_: a number 
+
+### clear 
+
+```Python
+clear(self)
+``` 
+clear the list of the pack
+
+
+
+_returns_: None 
+
+### do_actuate 
+
+```Python
+do_actuate(self, raw, naming_map, device=None)
+``` 
+
+called while parsing incoming data for a record that is not yet part of this pack object.
+adds a new record and raises the actuate callback of the pack with the newly created record as argument
+
+_parameters:_
+
+- naming_map:
+- `device:` optional: if the device was not found
+- `raw:` the raw record definition, as found in the json structure. this still has invalid labels.
+
+
+_returns_: None 
+
+### from_cbor 
+
+```Python
+from_cbor(self, data)
+``` 
+
+parse a cbor data byte array to a senml pack structure.
+
+_parameters:_
+
+- `data:` a byte array.
+
+
+_returns_: None 
+
+### from_json 
+
+```Python
+from_json(self, data)
+``` 
+
+parse a json string and convert it to a senml pack structure
+
+_parameters:_
+
+- `data:` a string containing json data.
+
+
+_returns_: None, will call the appropriate callback functions. 
+
+
+
+### remove 
+
+```Python
+remove(self, item)
+``` 
+removes the item from the pack
+
+
+_parameters:_
+
+- `item:` {SenmlRecord} the item that needs to be removed
+
+
+_returns_: None 
+
+### to_cbor 
+
+```Python
+to_cbor(self)
+``` 
+
+render the content of this object to a cbor byte array
+
+
+_returns_: a byte array 
+
+### to_json 
+
+```Python
+to_json(self)
+``` 
+
+render the content of this object to a string.
+
+
+_returns_: a string representing the senml pack object 
+
+## senml_pack.SenmlPackIterator Objects
+
+
+an iterator to walk over all records in a pack 
+
+### __init__ 
+
+```Python
+__init__(self, list)
+``` 
+
+
+
+### __iter__ 
+
+```Python
+__iter__(self)
+``` 
+
+
+
+### __next__ 
+
+```Python
+__next__(self)
+``` 
+
+
diff --git a/micropython/senml/docs/senml_record.md b/micropython/senml/docs/senml_record.md
new file mode 100644
index 000000000..6bac549a5
--- /dev/null
+++ b/micropython/senml/docs/senml_record.md
@@ -0,0 +1,86 @@
+
+# senml_record Module
+
+
+## senml_record.SenmlRecord Objects
+
+
+represents a single value in a senml pack object 
+
+### __enter__ 
+
+```Python
+__enter__(self)
+``` 
+
+for supporting the 'with' statement
+
+
+_returns_: self 
+
+### __exit__ 
+
+```Python
+__exit__(self, exc_type, exc_val, exc_tb)
+``` 
+
+when destroyed in a 'with' statement, make certain that the item is removed from the parent list.
+
+
+_returns_: None 
+
+### __init__ 
+
+```Python
+__init__(self, name, **kwargs)
+``` 
+
+create a new senml record
+
+_parameters:_
+
+- `kwargs:`  optional parameters:
+    - value: the value to store in the record
+    - time: the timestamp to use (when was the value measured)
+    - name: the name of hte record
+    - unit: unit value
+    - sum: sum value
+    - update_time: max time before sensor will provide an updated reading
+    - callback: a callback function taht will be called when actuator data has been found. Expects no params 
+
+### do_actuate 
+
+```Python
+do_actuate(self, raw, naming_map)
+``` 
+
+called when a raw senml record was found for this object. Stores the data and if there is a callback, calls it.
+
+_parameters:_
+
+- `raw:` raw senml object
+
+
+_returns_: None 
+
+### sum 
+
+
+
+### time 
+
+get the time at which the measurement for the record was taken.
+
+
+_returns_: a unix time stamp. This is the absolute value, not adjusted to the base time of the pack. 
+
+### update_time 
+
+get the time at which the next measurement is expected to be taken for this record.
+
+
+_returns_: a unix time stamp. This is the absolute value, not adjusted to the base time of the pack. 
+
+### value 
+
+get the value currently assigned to the object 
diff --git a/micropython/senml/docs/senml_unit.md b/micropython/senml/docs/senml_unit.md
new file mode 100644
index 000000000..816c40679
--- /dev/null
+++ b/micropython/senml/docs/senml_unit.md
@@ -0,0 +1,183 @@
+
+# senml_unit Module
+
+
+## Functions
+
+
+
+## senml_unit.SenmlUnits Objects
+
+
+
+
+##### `SENML_UNIT_ACCELERATION` 
+
+
+##### `SENML_UNIT_AMPERE` 
+
+
+##### `SENML_UNIT_BEATS` 
+
+
+##### `SENML_UNIT_BECQUEREL` 
+
+
+##### `SENML_UNIT_BEL` 
+
+
+##### `SENML_UNIT_BIT` 
+
+
+##### `SENML_UNIT_BIT_PER_SECOND` 
+
+
+##### `SENML_UNIT_BPM` 
+
+
+##### `SENML_UNIT_CANDELA` 
+
+
+##### `SENML_UNIT_CANDELA_PER_SQUARE_METER` 
+
+
+##### `SENML_UNIT_COULOMB` 
+
+
+##### `SENML_UNIT_COUNTER` 
+
+
+##### `SENML_UNIT_CUBIC_METER` 
+
+
+##### `SENML_UNIT_CUBIC_METER_PER_SECOND` 
+
+
+##### `SENML_UNIT_DECIBEL` 
+
+
+##### `SENML_UNIT_DECIBEL_RELATIVE_TO_1_W` 
+
+
+##### `SENML_UNIT_DEGREES_CELSIUS` 
+
+
+##### `SENML_UNIT_DEGREES_LATITUDE` 
+
+
+##### `SENML_UNIT_DEGREES_LONGITUDE` 
+
+
+##### `SENML_UNIT_EVENT_RATE_PER_MINUTE` 
+
+
+##### `SENML_UNIT_EVENT_RATE_PER_SECOND` 
+
+
+##### `SENML_UNIT_FARAD` 
+
+
+##### `SENML_UNIT_GRAM` 
+
+
+##### `SENML_UNIT_GRAY` 
+
+
+##### `SENML_UNIT_HENRY` 
+
+
+##### `SENML_UNIT_HERTZ` 
+
+
+##### `SENML_UNIT_JOULE` 
+
+
+##### `SENML_UNIT_KATAL` 
+
+
+##### `SENML_UNIT_KELVIN` 
+
+
+##### `SENML_UNIT_KILOGRAM` 
+
+
+##### `SENML_UNIT_LITER` 
+
+
+##### `SENML_UNIT_LITER_PER_SECOND` 
+
+
+##### `SENML_UNIT_LUMEN` 
+
+
+##### `SENML_UNIT_LUX` 
+
+
+##### `SENML_UNIT_METER` 
+
+
+##### `SENML_UNIT_MOLE` 
+
+
+##### `SENML_UNIT_NEWTON` 
+
+
+##### `SENML_UNIT_OHM` 
+
+
+##### `SENML_UNIT_PASCAL` 
+
+
+##### `SENML_UNIT_PERCENTAGE_REMAINING_BATTERY_LEVEL` 
+
+
+##### `SENML_UNIT_PH` 
+
+
+##### `SENML_UNIT_RADIAN` 
+
+
+##### `SENML_UNIT_RATIO` 
+
+
+##### `SENML_UNIT_RELATIVE_HUMIDITY` 
+
+
+##### `SENML_UNIT_SECOND` 
+
+
+##### `SENML_UNIT_SECONDS_REMAINING_BATTERY_LEVEL` 
+
+
+##### `SENML_UNIT_SIEMENS` 
+
+
+##### `SENML_UNIT_SIEMENS_PER_METER` 
+
+
+##### `SENML_UNIT_SIEVERT` 
+
+
+##### `SENML_UNIT_SQUARE_METER` 
+
+
+##### `SENML_UNIT_STERADIAN` 
+
+
+##### `SENML_UNIT_TESLA` 
+
+
+##### `SENML_UNIT_VELOCITY` 
+
+
+##### `SENML_UNIT_VOLT` 
+
+
+##### `SENML_UNIT_WATT` 
+
+
+##### `SENML_UNIT_WATT_PER_SQUARE_METER` 
+
+
+##### `SENML_UNIT_WEBER` 
+
diff --git a/micropython/senml/examples/actuator.py b/micropython/senml/examples/actuator.py
new file mode 100644
index 000000000..2fac474cd
--- /dev/null
+++ b/micropython/senml/examples/actuator.py
@@ -0,0 +1,66 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from senml import *
+
+
+def do_actuate(record):
+    """
+    called when actuate_me receives a value.
+    :return: None
+    """
+    print(record.value)
+
+
+def generic_callback(record, **kwargs):
+    """
+    a generic callback, attached to the device. Called when a record is found that has not yet been registered
+    in the pack. When this callback is called, the record will already be added to the pack.
+    :param record: the newly found record.
+    :return: None
+    """
+    print("found record: " + record.name)
+    print("with value: " + str(record.value))
+
+
+pack = SenmlPack("device_name", generic_callback)
+actuate_me = SenmlRecord("actuator", callback=do_actuate)
+
+pack.add(actuate_me)
+
+json_data = '[{"bn": "device_name", "n":"actuator", "v": 10 }]'
+print(json_data)
+pack.from_json(json_data)
+
+json_data = (
+    '[{"bn": "device_name", "n":"actuator", "v": 20 }, {"n": "another_actuator", "vs": "a value"}]'
+)
+print(json_data)
+pack.from_json(json_data)
+
+print('[{"bn": "device_name", "n":"temp", "v": 20, "u": "Cel" }]')
+# this represents the cbor json struct: [{-2: "device_name", 0: "temp", 1: "Cel", 2: 20}]
+cbor_data = bytes.fromhex("81A4216B6465766963655F6E616D65006474656D70016343656C0214")
+pack.from_cbor(cbor_data)
diff --git a/micropython/senml/examples/base.py b/micropython/senml/examples/base.py
new file mode 100644
index 000000000..6a49cfdd2
--- /dev/null
+++ b/micropython/senml/examples/base.py
@@ -0,0 +1,46 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from senml import *
+import time
+
+
+pack = SenmlPack("device_name")
+temp = SenmlRecord("temperature", unit=SenmlUnits.SENML_UNIT_DEGREES_CELSIUS, value=23.5)
+door_pos = SenmlRecord("doorPos", update_time=20, value=True)
+int_val = SenmlRecord("int_val", sum=100)
+
+pack.add(temp)
+pack.add(door_pos)
+pack.add(int_val)
+
+pack.base_time = time.time()
+pack.base_value = 5
+pack.base_sum = 50
+time.sleep(2)
+temp.time = time.time()
+
+
+print(pack.to_json())
diff --git a/micropython/senml/examples/basic.py b/micropython/senml/examples/basic.py
new file mode 100644
index 000000000..3f3ed6150
--- /dev/null
+++ b/micropython/senml/examples/basic.py
@@ -0,0 +1,38 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from senml import *
+import time
+
+
+pack = SenmlPack("device")
+
+while True:
+    with SenmlRecord(
+        "test", value=1
+    ) as rec:  # use a with statement to automatically remove the item from the list when it goes out of scope
+        pack.add(rec)
+        print(pack.to_json())
+    time.sleep(1)
diff --git a/micropython/senml/examples/basic2.py b/micropython/senml/examples/basic2.py
new file mode 100644
index 000000000..ca53b4a6e
--- /dev/null
+++ b/micropython/senml/examples/basic2.py
@@ -0,0 +1,44 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from senml import *
+import time
+
+
+pack = SenmlPack("device_name")
+temp = SenmlRecord("temperature", unit=SenmlUnits.SENML_UNIT_DEGREES_CELSIUS, value=23.5)
+door_pos = SenmlRecord("doorPos", update_time=20, value=True)
+str_val = SenmlRecord("str val")
+
+pack.add(temp)
+pack.add(door_pos)
+pack.add(str_val)
+
+while True:
+    temp.value = temp.value + 1.1
+    door_pos.value = not door_pos.value
+    str_val.value = "test"
+    print(pack.to_json())
+    time.sleep(1)
diff --git a/micropython/senml/examples/basic_cbor.py b/micropython/senml/examples/basic_cbor.py
new file mode 100644
index 000000000..b9d9d620b
--- /dev/null
+++ b/micropython/senml/examples/basic_cbor.py
@@ -0,0 +1,41 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from senml import *
+import time
+import cbor2
+
+pack = SenmlPack("device_name")
+
+while True:
+    with SenmlRecord(
+        "test", value=10
+    ) as rec:  # use a with statement to automatically remove the item from the list when it goes out of scope, generate a value for the record
+        pack.add(rec)
+        cbor_val = pack.to_cbor()
+        print(cbor_val)
+        print(cbor_val.hex())
+        print(cbor2.loads(cbor_val))  # convert to string again so we can print it.
+    time.sleep(1)
diff --git a/micropython/senml/examples/custom_record.py b/micropython/senml/examples/custom_record.py
new file mode 100644
index 000000000..1e83ea06b
--- /dev/null
+++ b/micropython/senml/examples/custom_record.py
@@ -0,0 +1,132 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from senml import *
+
+import time
+
+
+class Coordinates(SenmlRecord):
+    def __init__(self, name, **kwargs):
+        """overriding the init function so we can initiate the 3 senml records that will represent lat,lon, alt"""
+        self._lat = SenmlRecord(
+            "lattitude", unit=SenmlUnits.SENML_UNIT_DEGREES_LATITUDE
+        )  # create these befor calling base constructor so that all can be init correctly from constructor
+        self._lon = SenmlRecord("longitude", unit=SenmlUnits.SENML_UNIT_DEGREES_LONGITUDE)
+        self._alt = SenmlRecord("altitude", unit=SenmlUnits.SENML_UNIT_METER)
+        super(Coordinates, self).__init__(
+            name, **kwargs
+        )  # need to call base init, to make certain all is ok.
+
+    def _check_value_type(self, value):
+        """overriding the check on value type to make certain that only an array with 3 values is assigned: lat,lon/alt"""
+        if value is not None:
+            if not isinstance(value, list):
+                raise Exception("invalid data type: array with 3 elements expected lat, lon, alt")
+
+    def _build_rec_dict(self, naming_map, appendTo):
+        """
+        override the rendering of the senml data objects. These will be converted to json or cbor
+        :param naming_map: {dictionary} a map that determines the field names, these are different for json vs cbor
+        :param appendTo: {list} the result list
+        :return: None
+        """
+        self._lat._build_rec_dict(naming_map, appendTo)
+        self._lon._build_rec_dict(naming_map, appendTo)
+        self._alt._build_rec_dict(naming_map, appendTo)
+
+    @SenmlRecord.value.setter
+    def value(self, value):
+        """set the current value.
+        this is overridden so we can pass on the values to the internal objects. It's also stored in the parent
+        so that a 'get-value' still returns the array.
+        """
+        self._value = (
+            value  # micropython doesn't support calling setter of parent property, do it manually
+        )
+        if value:
+            self._lat.value = value[0]
+            self._lon.value = value[1]
+            self._alt.value = value[2]
+        else:
+            self._lat.value = None
+            self._lon.value = None
+            self._alt.value = None
+
+    @SenmlRecord.time.setter
+    def time(self, value):
+        """set the time stamp.
+        this is overridden so we can pass on the values to the internal objects.
+        """
+        self._check_number_type(
+            value, "time"
+        )  # micropython doesn't support calling setter of parent property, do it manually
+        self._time = value
+        self._lat.time = value
+        self._lon.time = value
+        self._alt.time = value
+
+    @SenmlRecord.update_time.setter
+    def update_time(self, value):
+        """set the time stamp.
+        this is overridden so we can pass on the values to the internal objects.
+        """
+        self._check_number_type(
+            value, "update_time"
+        )  # micropython doesn't support calling setter of parent property, do it manually
+        self._update_time = value
+        self._lat.update_time = value
+        self._lon.update_time = value
+        self._alt.update_time = value
+
+    @SenmlRecord._parent.setter
+    def _parent(self, value):
+        """set the time stamp.
+        this is overridden so we can pass on the values to the internal objects.
+        This is needed so that the child objects can correctly take base time (optionally also base-sum, base-value) into account
+        """
+        self.__parent = (
+            value  # micropython doesn't support calling setter of parent property, do it manually
+        )
+        self._lat._parent = value
+        self._lon._parent = value
+        self._alt._parent = value
+
+
+pack = SenmlPack("device_name")
+loc = Coordinates("location")
+loc2 = Coordinates("location", value=[52.0259, 5.4775, 230])
+pack.add(loc)
+pack.add(loc2)
+
+print(loc._parent.name)
+
+loc.value = [51.0259, 4.4775, 10]
+print(pack.to_json())
+
+pack.base_time = time.time()  # set a base time
+time.sleep(2)
+loc.time = time.time()  # all child objects will receive the time value
+print(pack.to_json())
diff --git a/micropython/senml/examples/gateway.py b/micropython/senml/examples/gateway.py
new file mode 100644
index 000000000..e1827ff2d
--- /dev/null
+++ b/micropython/senml/examples/gateway.py
@@ -0,0 +1,49 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from senml import *
+import time
+
+gateway_pack = SenmlPack("gateway")
+
+dev1_pack = SenmlPack("dev1")
+dev2_pack = SenmlPack("dev2")
+
+temp = SenmlRecord("temperature", unit=SenmlUnits.SENML_UNIT_DEGREES_CELSIUS, value=23.5)
+door_pos = SenmlRecord("doorPos", update_time=20, value=True)
+str_val = SenmlRecord("str val")
+
+gateway_pack.add(temp)
+gateway_pack.add(dev1_pack)
+gateway_pack.add(dev2_pack)
+dev1_pack.add(door_pos)
+dev2_pack.add(str_val)
+
+while True:
+    temp.value = temp.value + 1.1
+    door_pos.value = not door_pos.value
+    str_val.value = "test"
+    print(gateway_pack.to_json())
+    time.sleep(1)
diff --git a/micropython/senml/examples/gateway_actuators.py b/micropython/senml/examples/gateway_actuators.py
new file mode 100644
index 000000000..a7e5b378c
--- /dev/null
+++ b/micropython/senml/examples/gateway_actuators.py
@@ -0,0 +1,74 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from senml import *
+
+
+def do_actuate(record):
+    """
+    called when actuate_me receives a value.
+    :return: None
+    """
+    print("for known device: ")
+    print(record.value)
+
+
+def device_callback(record, **kwargs):
+    """
+    a generic callback, attached to the device. Called when a record is found that has not yet been registered
+    in the pack. When this callback is called, the record will already be added to the pack.
+    :param kwargs: optional extra parameters
+    :param record: the newly found record.
+    :return: None
+    """
+    print("found record: " + record.name)
+    print("with value: " + record.value)
+
+
+def gateway_callback(record, **kwargs):
+    """
+    a generic callback, attached to the device. Called when a record is found that has not yet been registered
+    in the pack. When this callback is called, the record will already be added to the pack.
+    :param record: the newly found record.
+    :param kwargs: optional extra parameters (device can be found here)
+    :return: None
+    """
+    if "device" in kwargs and kwargs["device"] is not None:
+        print("for device: " + kwargs["device"].name)
+    else:
+        print("for gateway: ")
+    print("found record: " + record.name)
+    print("with value: " + str(record.value))
+
+
+gateway = SenmlPack("gateway_name", gateway_callback)
+device = SenmlPack("device_name", device_callback)
+actuate_me = SenmlRecord("actuator", callback=do_actuate)
+
+gateway.add(device)
+device.add(actuate_me)
+gateway.from_json(
+    '[{"bn": "gateway_name", "n":"temp", "v": 22},{"n": "gateway_actuator", "vb": true}, {"bn": "device_name", "n":"actuator", "v": 20 }, {"n": "another_actuator", "vs": "a value"}, {"bn": "device_2", "n":"temp", "v": 20 }, {"n": "actuator2", "vs": "value2"}]'
+)
diff --git a/micropython/senml/examples/supported_data_types.py b/micropython/senml/examples/supported_data_types.py
new file mode 100644
index 000000000..94976bb66
--- /dev/null
+++ b/micropython/senml/examples/supported_data_types.py
@@ -0,0 +1,52 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from senml import *
+import time
+
+pack = SenmlPack("device_name")
+
+double_val = SenmlRecord("double", value=23.5)
+int_val = SenmlRecord("int", value=23)
+bool_val = SenmlRecord("bool", value=True)
+str_val = SenmlRecord("str val", value="test")
+bytes_val = SenmlRecord("bytes", value=bytearray(b"00 1e 05 ff"))
+
+# invalid value
+try:
+    invalid = SenmlRecord("invalid", value={"a": 1})
+except Exception as error:
+    print(error)
+
+
+pack.add(double_val)
+pack.add(int_val)
+pack.add(bool_val)
+pack.add(str_val)
+pack.add(bytes_val)
+
+while True:
+    print(pack.to_json())
+    time.sleep(1)
diff --git a/micropython/senml/manifest.py b/micropython/senml/manifest.py
new file mode 100644
index 000000000..f4743075a
--- /dev/null
+++ b/micropython/senml/manifest.py
@@ -0,0 +1,9 @@
+metadata(
+    description="SenML serialisation for MicroPython.",
+    version="0.1.1",
+    pypi_publish="micropython-senml",
+)
+
+require("cbor2")
+
+package("senml")
diff --git a/micropython/senml/senml/__init__.py b/micropython/senml/senml/__init__.py
new file mode 100644
index 000000000..908375fdb
--- /dev/null
+++ b/micropython/senml/senml/__init__.py
@@ -0,0 +1,29 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from .senml_base import SenmlBase
+from .senml_pack import SenmlPack
+from .senml_record import SenmlRecord
+from .senml_unit import SenmlUnits
diff --git a/micropython/senml/senml/senml_base.py b/micropython/senml/senml/senml_base.py
new file mode 100644
index 000000000..b277c9477
--- /dev/null
+++ b/micropython/senml/senml/senml_base.py
@@ -0,0 +1,30 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+
+class SenmlBase(object):
+    """
+    the base class for all senml objects.
+    """
diff --git a/micropython/senml/senml/senml_pack.py b/micropython/senml/senml/senml_pack.py
new file mode 100644
index 000000000..5a0554467
--- /dev/null
+++ b/micropython/senml/senml/senml_pack.py
@@ -0,0 +1,358 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from senml.senml_record import SenmlRecord
+from senml.senml_base import SenmlBase
+import json
+import cbor2
+
+
+class SenmlPackIterator:
+    """an iterator to walk over all records in a pack"""
+
+    def __init__(self, list):
+        self._list = list
+        self._index = 0
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        if self._index < len(self._list):
+            res = self._list[self._index]
+            self._index += 1
+            return res
+        else:
+            raise StopIteration
+
+
+class SenmlPack(SenmlBase):
+    """
+    represents a sneml pack object. This can contain multiple records but also other (child) pack objects.
+    When the pack object only contains records, it represents the data of a device.
+    If the pack object has child pack objects, then it represents a gateway
+    """
+
+    json_mappings = {
+        "bn": "bn",
+        "bt": "bt",
+        "bu": "bu",
+        "bv": "bv",
+        "bs": "bs",
+        "n": "n",
+        "u": "u",
+        "v": "v",
+        "vs": "vs",
+        "vb": "vb",
+        "vd": "vd",
+        "s": "s",
+        "t": "t",
+        "ut": "ut",
+    }
+
+    def __init__(self, name, callback=None):
+        """
+        initialize the object
+        :param name: {string} the name of the pack
+        """
+        self._data = []
+        self.name = name
+        self._base_value = None
+        self._base_time = None
+        self._base_sum = None
+        self.base_unit = None
+        self._parent = None  # a pack can also be the child of another pack.
+        self.actuate = callback  # actuate callback function
+
+    def __iter__(self):
+        return SenmlPackIterator(self._data)
+
+    def __enter__(self):
+        """
+        for supporting the 'with' statement
+        :return: self
+        """
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """
+        when destroyed in a 'with' statement, make certain that the item is removed from the parent list.
+        :return: None
+        """
+        if self._parent:
+            self._parent.remove(self)
+
+    @property
+    def base_value(self):
+        """
+        the base value of the pack.
+        :return: a number
+        """
+        return self._base_value
+
+    @base_value.setter
+    def base_value(self, value):
+        """
+        set the base value.
+        :param value: only number allowed
+        :return:
+        """
+        self._check_value_type(value, "base_value")
+        self._base_value = value
+
+    @property
+    def base_sum(self):
+        """
+        the base sum of the pack.
+        :return: a number
+        """
+        return self._base_sum
+
+    @base_sum.setter
+    def base_sum(self, value):
+        """
+        set the base value.
+        :param value: only number allowed
+        :return:
+        """
+        self._check_value_type(value, "base_sum")
+        self._base_sum = value
+
+    @property
+    def base_time(self):
+        return self._base_time
+
+    @base_time.setter
+    def base_time(self, value):
+        self._check_value_type(value, "base_time")
+        self._base_time = value
+
+    def _check_value_type(self, value, field_name):
+        """
+        checks if the type of value is allowed for senml
+        :return: None, raisee exception if not ok.
+        """
+        if value is not None:
+            if not (isinstance(value, int) or isinstance(value, float)):
+                raise Exception("invalid type for " + field_name + ", only numbers allowed")
+
+    def from_json(self, data):
+        """
+        parse a json string and convert it to a senml pack structure
+        :param data: a string containing json data.
+        :return: None, will r
+        """
+        records = json.loads(data)  # load the raw senml data
+        self._process_incomming_data(records, SenmlPack.json_mappings)
+
+    def _process_incomming_data(self, records, naming_map):
+        """
+        generic processor for incomming data (actuators.
+        :param records: the list of raw senml data, parsed from a json or cbor structure
+        :param naming_map: translates cbor to json field names (when needed).
+        :return: None
+        """
+        cur_pack_el = self
+        new_pack = False
+        for item in records:
+            if naming_map["bn"] in item:  # ref to a pack element, either this or a child pack.
+                if item[naming_map["bn"]] != self.name:
+                    pack_el = [x for x in self._data if x.name == item[naming_map["bn"]]]
+                else:
+                    pack_el = [self]
+                if len(pack_el) > 0:
+                    cur_pack_el = pack_el[0]
+                    new_pack = False
+                else:
+                    device = SenmlPack(item[naming_map["bn"]])
+                    self._data.append(device)
+                    cur_pack_el = device
+                    new_pack = True
+
+                if (
+                    naming_map["bv"] in item
+                ):  # need to copy the base value assigned to the pack element so we can do proper conversion for actuators.
+                    cur_pack_el.base_value = item[naming_map["bv"]]
+
+                rec_el = [x for x in cur_pack_el._data if x.name == item[naming_map["n"]]]
+                if len(rec_el) > 0:
+                    rec_el[0].do_actuate(item, naming_map)
+                elif new_pack:
+                    self.do_actuate(item, naming_map, cur_pack_el)
+                else:
+                    cur_pack_el.do_actuate(item, naming_map)
+            else:
+                rec_el = [x for x in self._data if x.name == item[naming_map["n"]]]
+                if len(rec_el) > 0:
+                    rec_el[0].do_actuate(item, naming_map)
+                elif new_pack:
+                    self.do_actuate(item, naming_map, cur_pack_el)
+                else:
+                    cur_pack_el.do_actuate(item, naming_map)
+
+    def do_actuate(self, raw, naming_map, device=None):
+        """
+        called while parsing incoming data for a record that is not yet part of this pack object.
+        adds a new record and raises the actuate callback of the pack with the newly created record as argument
+        :param naming_map:
+        :param device: optional: if the device was not found
+        :param raw: the raw record definition, as found in the json structure. this still has invalid labels.
+        :return: None
+        """
+        rec = SenmlRecord(raw[naming_map["n"]])
+        if device:
+            device.add(rec)
+            rec._from_raw(raw, naming_map)
+            if self.actuate:
+                self.actuate(rec, device=device)
+        else:
+            self.add(rec)
+            rec._from_raw(raw, naming_map)
+            if self.actuate:
+                self.actuate(rec, device=None)
+
+    def to_json(self):
+        """
+        render the content of this object to a string.
+        :return: a string representing the senml pack object
+        """
+        converted = []
+        self._build_rec_dict(SenmlPack.json_mappings, converted)
+        return json.dumps(converted)
+
+    def _build_rec_dict(self, naming_map, appendTo):
+        """
+        converts the object to a senml object with the proper naming in place.
+        This can be recursive: a pack can contain other packs.
+        :param naming_map: a dictionary used to pick the correct field names for either senml json or senml cbor
+        :return:
+        """
+        internalList = []
+        for item in self._data:
+            item._build_rec_dict(naming_map, internalList)
+        if len(internalList) > 0:
+            first_rec = internalList[0]
+        else:
+            first_rec = {}
+            internalList.append(first_rec)
+
+        if self.name:
+            first_rec[naming_map["bn"]] = self.name
+        if self.base_value:
+            first_rec[naming_map["bv"]] = self.base_value
+        if self.base_unit:
+            first_rec[naming_map["bu"]] = self.base_unit
+        if self.base_sum:
+            first_rec[naming_map["bs"]] = self.base_sum
+        if self.base_time:
+            first_rec[naming_map["bt"]] = self.base_time
+        appendTo.extend(internalList)
+
+    def from_cbor(self, data):
+        """
+        parse a cbor data byte array to a senml pack structure.
+        :param data: a byte array.
+        :return: None
+        """
+        records = cbor2.loads(data)  # load the raw senml data
+        naming_map = {
+            "bn": -2,
+            "bt": -3,
+            "bu": -4,
+            "bv": -5,
+            "bs": -16,
+            "n": 0,
+            "u": 1,
+            "v": 2,
+            "vs": 3,
+            "vb": 4,
+            "vd": 8,
+            "s": 5,
+            "t": 6,
+            "ut": 7,
+        }
+        self._process_incomming_data(records, naming_map)
+
+    def to_cbor(self):
+        """
+        render the content of this object to a cbor byte array
+        :return: a byte array
+        """
+        naming_map = {
+            "bn": -2,
+            "bt": -3,
+            "bu": -4,
+            "bv": -5,
+            "bs": -16,
+            "n": 0,
+            "u": 1,
+            "v": 2,
+            "vs": 3,
+            "vb": 4,
+            "vd": 8,
+            "s": 5,
+            "t": 6,
+            "ut": 7,
+        }
+        converted = []
+        self._build_rec_dict(naming_map, converted)
+        return cbor2.dumps(converted)
+
+    def add(self, item):
+        """
+        adds the item to the list of records
+        :param item: {SenmlRecord} the item that needs to be added to the pack
+        :return: None
+        """
+        if not (isinstance(item, SenmlBase)):
+            raise Exception("invalid type of param, SenmlRecord or SenmlPack expected")
+        if item._parent is not None:
+            raise Exception("item is already part of a pack")
+
+        self._data.append(item)
+        item._parent = self
+
+    def remove(self, item):
+        """
+        removes the item from the list of records
+        :param item: {SenmlRecord} the item that needs to be removed
+        :return: None
+        """
+        if not (isinstance(item, SenmlBase)):
+            raise Exception("invalid type of param, SenmlRecord or SenmlPack expected")
+        if not item._parent == self:
+            raise Exception("item is not part of this pack")
+
+        self._data.remove(item)
+        item._parent = None
+
+    def clear(self):
+        """
+        clear the list of the pack
+        :return: None
+        """
+        for item in self._data:
+            item._parent = None
+        self._data = []
diff --git a/micropython/senml/senml/senml_record.py b/micropython/senml/senml/senml_record.py
new file mode 100644
index 000000000..ae40f0f70
--- /dev/null
+++ b/micropython/senml/senml/senml_record.py
@@ -0,0 +1,246 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import binascii
+from senml.senml_base import SenmlBase
+
+
+class SenmlRecord(SenmlBase):
+    """represents a single value in a senml pack object"""
+
+    def __init__(self, name, **kwargs):
+        """
+        create a new senml record
+        :param kwargs:  optional parameters:
+            - value: the value to store in the record
+            - time: the timestamp to use (when was the value measured)
+            - name: the name of hte record
+            - unit: unit value
+            - sum: sum value
+            - update_time: max time before sensor will provide an updated reading
+            - callback: a callback function taht will be called when actuator data has been found. Expects no params
+        """
+        self.__parent = None  # using double __ cause it's a field for an internal property
+        self._unit = None  # declare and init internal fields
+        self._value = None
+        self._time = None
+        self._sum = None
+        self._update_time = None
+
+        self._parent = None  # internal reference to the parent object
+        self.name = name
+        self.unit = kwargs.get("unit", None)
+        self.value = kwargs.get("value", None)
+        self.time = kwargs.get("time", None)
+        self.sum = kwargs.get("sum", None)
+        self.update_time = kwargs.get("update_time", None)
+        self.actuate = kwargs.get("callback", None)  # actuate callback function
+
+    def __enter__(self):
+        """
+        for supporting the 'with' statement
+        :return: self
+        """
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """
+        when destroyed in a 'with' statement, make certain that the item is removed from the parent list.
+        :return: None
+        """
+        if self._parent:
+            self._parent.remove(self)
+
+    def _check_value_type(self, value):
+        """
+        checks if the type of value is allowed for senml
+        :return: None, raisee exception if not ok.
+        """
+        if value is not None:
+            if not (
+                isinstance(value, bool)
+                or isinstance(value, int)
+                or isinstance(value, float)
+                or isinstance(value, bytearray)
+                or isinstance(value, str)
+            ):
+                raise Exception(
+                    "invalid type for value, only numbers, strings, boolean and byte arrays allowed"
+                )
+
+    def _check_number_type(self, value, field_name):
+        """
+        checks if the type of value is allowed for senml
+        :return: None, raisee exception if not ok.
+        """
+        if value is not None:
+            if not (isinstance(value, int) or isinstance(value, float)):
+                raise Exception("invalid type for " + field_name + ", only numbers allowed")
+
+    @property
+    def value(self):
+        """get the value currently assigned to the object"""
+        return self._value
+
+    @value.setter
+    def value(self, value):
+        """set the current value. Will not automatically update the time stamp. This has to be done seperatly for more
+        finegrained control
+        Note: when the value is a float, you can control rounding in the rendered output by using the function
+        round() while assigning the value. ex: record.value = round(12.2 / 1.5423, 2)
+        """
+        self._check_value_type(value)
+        self._value = value
+
+    @property
+    def time(self):
+        return self._time
+
+    @time.setter
+    def time(self, value):
+        self._check_number_type(value, "time")
+        self._time = value
+
+    @property
+    def update_time(self):
+        return self._update_time
+
+    @update_time.setter
+    def update_time(self, value):
+        self._check_number_type(value, "update_time")
+        self._update_time = value
+
+    @property
+    def sum(self):
+        return self._sum
+
+    @sum.setter
+    def sum(self, value):
+        self._check_number_type(value, "sum")
+        self._sum = value
+
+    @property
+    def _parent(self):
+        """
+        the parent pack object for this record. This is a property so that inheriters can override and do custom
+        actions when the parent is set (like passing it on to their children
+        :return:
+        """
+        return self.__parent
+
+    @_parent.setter
+    def _parent(self, value):
+        """
+        the parent pack object for this record. This is a property so that inheriters can override and do custom
+        actions when the parent is set (like passing it on to their children
+        :return:
+        """
+        self.__parent = value
+
+    def _build_rec_dict(self, naming_map, appendTo):
+        """
+        converts the object to a dictionary that can be rendered to senml.
+        :param naming_map: a dictionary that maps the field names to senml json or senml cbor. keys are in the
+        form 'n', 'v',...  values for 'n' are either 'n' or 0 (number is for cbor)
+        :return: a senml dictionary representation of the record
+        """
+        result = {}
+
+        if self.name:
+            result[naming_map["n"]] = self.name
+
+        if self._sum:
+            if self._parent and self._parent.base_sum:
+                result[naming_map["s"]] = self._sum - self._parent.base_sum
+            else:
+                result[naming_map["s"]] = self._sum
+        elif isinstance(self._value, bool):
+            result[naming_map["vb"]] = self._value
+        elif isinstance(self._value, int) or isinstance(self._value, float):
+            if self._parent and self._parent.base_value:
+                result[naming_map["v"]] = self._value - self._parent.base_value
+            else:
+                result[naming_map["v"]] = self._value
+        elif isinstance(self._value, str):
+            result[naming_map["vs"]] = self._value
+        elif isinstance(self._value, bytearray):
+            if (
+                naming_map["vd"] == "vd"
+            ):  # neeed to make a distinction between json (needs base64) and cbor (needs binary)
+                result[naming_map["vd"]] = binascii.b2a_base64(self._value, newline=False).decode(
+                    "utf8"
+                )
+            else:
+                result[naming_map["vd"]] = self._value
+        else:
+            raise Exception("sum or value of type bootl, number, string or byte-array is required")
+
+        if self._time:
+            if self._parent and self._parent.base_time:
+                result[naming_map["t"]] = self._time - self._parent.base_time
+            else:
+                result[naming_map["t"]] = self._time
+
+        if self.unit:
+            result[naming_map["u"]] = self.unit
+
+        if self._update_time:
+            if self._parent and self._parent.base_time:
+                result[naming_map["ut"]] = self._update_time - self._parent.base_time
+            else:
+                result[naming_map["ut"]] = self._update_time
+
+        appendTo.append(result)
+
+    def _from_raw(self, raw, naming_map):
+        """
+        extracts te data from the raw record. Used during parsing of incoming data.
+        :param raw: a raw senml record which still contains the original field names
+        :param naming_map: used to map cbor names to json field names
+        :return:
+        """
+        if naming_map["v"] in raw:
+            val = raw[naming_map["v"]]
+            if self._parent and self._parent.base_value:
+                val += self._parent.base_value
+        elif naming_map["vs"] in raw:
+            val = raw[naming_map["vs"]]
+        elif naming_map["vb"] in raw:
+            val = raw[naming_map["vb"]]
+        elif naming_map["vd"] in raw:
+            val = binascii.a2b_base64(raw[naming_map["vb"]])
+        else:
+            val = None
+        self.value = val
+
+    def do_actuate(self, raw, naming_map):
+        """
+        called when a raw senml record was found for this object. Stores the data and if there is a callback, calls it.
+        :param raw: raw senml object
+        :return: None
+        """
+        self._from_raw(raw, naming_map)
+        if self.actuate:
+            self.actuate(self)
diff --git a/micropython/senml/senml/senml_unit.py b/micropython/senml/senml/senml_unit.py
new file mode 100644
index 000000000..bf7753c4d
--- /dev/null
+++ b/micropython/senml/senml/senml_unit.py
@@ -0,0 +1,89 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+
+def enum(**enums):
+    return type("Enum", (), enums)
+
+
+SenmlUnits = enum(
+    SENML_UNIT_METER="m",
+    SENML_UNIT_KILOGRAM="kg",
+    SENML_UNIT_GRAM="g",
+    SENML_UNIT_SECOND="s",
+    SENML_UNIT_AMPERE="A",
+    SENML_UNIT_KELVIN="K",
+    SENML_UNIT_CANDELA="cd",
+    SENML_UNIT_MOLE="mol",
+    SENML_UNIT_HERTZ="Hz",
+    SENML_UNIT_RADIAN="rad",
+    SENML_UNIT_STERADIAN="sr",
+    SENML_UNIT_NEWTON="N",
+    SENML_UNIT_PASCAL="Pa",
+    SENML_UNIT_JOULE="J",
+    SENML_UNIT_WATT="W",
+    SENML_UNIT_COULOMB="C",
+    SENML_UNIT_VOLT="V",
+    SENML_UNIT_FARAD="F",
+    SENML_UNIT_OHM="Ohm",
+    SENML_UNIT_SIEMENS="S",
+    SENML_UNIT_WEBER="Wb",
+    SENML_UNIT_TESLA="T",
+    SENML_UNIT_HENRY="H",
+    SENML_UNIT_DEGREES_CELSIUS="Cel",
+    SENML_UNIT_LUMEN="lm",
+    SENML_UNIT_LUX="lx",
+    SENML_UNIT_BECQUEREL="Bq",
+    SENML_UNIT_GRAY="Gy",
+    SENML_UNIT_SIEVERT="Sv",
+    SENML_UNIT_KATAL="kat",
+    SENML_UNIT_SQUARE_METER="m2",
+    SENML_UNIT_CUBIC_METER="m3",
+    SENML_UNIT_LITER="l",
+    SENML_UNIT_VELOCITY="m/s",
+    SENML_UNIT_ACCELERATION="m/s2",
+    SENML_UNIT_CUBIC_METER_PER_SECOND="m3/s",
+    SENML_UNIT_LITER_PER_SECOND="l/s",
+    SENML_UNIT_WATT_PER_SQUARE_METER="W/m2",
+    SENML_UNIT_CANDELA_PER_SQUARE_METER="cd/m2",
+    SENML_UNIT_BIT="bit",
+    SENML_UNIT_BIT_PER_SECOND="bit/s",
+    SENML_UNIT_DEGREES_LATITUDE="lat",
+    SENML_UNIT_DEGREES_LONGITUDE="lon",
+    SENML_UNIT_PH="pH",
+    SENML_UNIT_DECIBEL="db",
+    SENML_UNIT_DECIBEL_RELATIVE_TO_1_W="dBW",
+    SENML_UNIT_BEL="Bspl",
+    SENML_UNIT_COUNTER="count",
+    SENML_UNIT_RATIO="//",
+    SENML_UNIT_RELATIVE_HUMIDITY="%RH",
+    SENML_UNIT_PERCENTAGE_REMAINING_BATTERY_LEVEL="%EL",
+    SENML_UNIT_SECONDS_REMAINING_BATTERY_LEVEL="EL",
+    SENML_UNIT_EVENT_RATE_PER_SECOND="1/s",
+    SENML_UNIT_EVENT_RATE_PER_MINUTE="1/min",
+    SENML_UNIT_BPM="beat/min",
+    SENML_UNIT_BEATS="beats",
+    SENML_UNIT_SIEMENS_PER_METER="S/m",
+)
diff --git a/micropython/uaiohttpclient/README b/micropython/uaiohttpclient/README
new file mode 100644
index 000000000..1222f9d61
--- /dev/null
+++ b/micropython/uaiohttpclient/README
@@ -0,0 +1,4 @@
+uaiohttpclient is an HTTP client module for MicroPython asyncio module,
+with API roughly compatible with aiohttp (https://github.com/KeepSafe/aiohttp)
+module. Note that only client is implemented, for server see picoweb
+microframework.
diff --git a/micropython/uaiohttpclient/example.py b/micropython/uaiohttpclient/example.py
new file mode 100644
index 000000000..540d1b3de
--- /dev/null
+++ b/micropython/uaiohttpclient/example.py
@@ -0,0 +1,16 @@
+#
+# uaiohttpclient - fetch URL passed as command line argument.
+#
+import sys
+import asyncio
+import uaiohttpclient as aiohttp
+
+
+async def run(url):
+    resp = await aiohttp.request("GET", url)
+    print(resp)
+    print(await resp.read())
+
+
+url = sys.argv[1]
+asyncio.run(run(url))
diff --git a/micropython/uaiohttpclient/manifest.py b/micropython/uaiohttpclient/manifest.py
new file mode 100644
index 000000000..8b35e0a70
--- /dev/null
+++ b/micropython/uaiohttpclient/manifest.py
@@ -0,0 +1,5 @@
+metadata(description="HTTP client module for MicroPython asyncio module", version="0.5.2")
+
+# Originally written by Paul Sokolovsky.
+
+module("uaiohttpclient.py")
diff --git a/micropython/uaiohttpclient/uaiohttpclient.py b/micropython/uaiohttpclient/uaiohttpclient.py
new file mode 100644
index 000000000..2e782638c
--- /dev/null
+++ b/micropython/uaiohttpclient/uaiohttpclient.py
@@ -0,0 +1,104 @@
+import asyncio
+
+
+class ClientResponse:
+    def __init__(self, reader):
+        self.content = reader
+
+    async def read(self, sz=-1):
+        return await self.content.read(sz)
+
+    def __repr__(self):
+        return "<ClientResponse %d %s>" % (self.status, self.headers)
+
+
+class ChunkedClientResponse(ClientResponse):
+    def __init__(self, reader):
+        self.content = reader
+        self.chunk_size = 0
+
+    async def read(self, sz=4 * 1024 * 1024):
+        if self.chunk_size == 0:
+            line = await self.content.readline()
+            # print("chunk line:", l)
+            line = line.split(b";", 1)[0]
+            self.chunk_size = int(line, 16)
+            # print("chunk size:", self.chunk_size)
+            if self.chunk_size == 0:
+                # End of message
+                sep = await self.content.read(2)
+                assert sep == b"\r\n"
+                return b""
+        data = await self.content.read(min(sz, self.chunk_size))
+        self.chunk_size -= len(data)
+        if self.chunk_size == 0:
+            sep = await self.content.read(2)
+            assert sep == b"\r\n"
+        return data
+
+    def __repr__(self):
+        return "<ChunkedClientResponse %d %s>" % (self.status, self.headers)
+
+
+async def request_raw(method, url):
+    try:
+        proto, dummy, host, path = url.split("/", 3)
+    except ValueError:
+        proto, dummy, host = url.split("/", 2)
+        path = ""
+
+    if ":" in host:
+        host, port = host.split(":")
+        port = int(port)
+    else:
+        port = 80
+
+    if proto != "http:":
+        raise ValueError("Unsupported protocol: " + proto)
+    reader, writer = await asyncio.open_connection(host, port)
+    # Use protocol 1.0, because 1.1 always allows to use chunked
+    # transfer-encoding But explicitly set Connection: close, even
+    # though this should be default for 1.0, because some servers
+    # misbehave w/o it.
+    query = "%s /%s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\nUser-Agent: compat\r\n\r\n" % (
+        method,
+        path,
+        host,
+    )
+    await writer.awrite(query.encode("latin-1"))
+    return reader
+
+
+async def request(method, url):
+    redir_cnt = 0
+    while redir_cnt < 2:
+        reader = await request_raw(method, url)
+        headers = []
+        sline = await reader.readline()
+        sline = sline.split(None, 2)
+        status = int(sline[1])
+        chunked = False
+        while True:
+            line = await reader.readline()
+            if not line or line == b"\r\n":
+                break
+            headers.append(line)
+            if line.startswith(b"Transfer-Encoding:"):
+                if b"chunked" in line:
+                    chunked = True
+            elif line.startswith(b"Location:"):
+                url = line.rstrip().split(None, 1)[1].decode("latin-1")
+
+        if 301 <= status <= 303:
+            redir_cnt += 1
+            await reader.aclose()
+            continue
+        break
+
+    if chunked:
+        resp = ChunkedClientResponse(reader)
+    else:
+        resp = ClientResponse(reader)
+    resp.status = status
+    resp.headers = headers
+    return resp
diff --git a/micropython/ucontextlib/manifest.py b/micropython/ucontextlib/manifest.py
new file mode 100644
index 000000000..81e6e0859
--- /dev/null
+++ b/micropython/ucontextlib/manifest.py
@@ -0,0 +1,5 @@
+metadata(
+    description="Minimal subset of contextlib for MicroPython low-memory ports", version="0.1.1"
+)
+
+module("ucontextlib.py")
diff --git a/micropython/ucontextlib/tests.py b/micropython/ucontextlib/tests.py
new file mode 100644
index 000000000..163175d82
--- /dev/null
+++ b/micropython/ucontextlib/tests.py
@@ -0,0 +1,35 @@
+import unittest
+from ucontextlib import contextmanager
+
+
+class ContextManagerTestCase(unittest.TestCase):
+    def setUp(self):
+        self._history = []
+
+        @contextmanager
+        def manager(x):
+            self._history.append("start")
+            try:
+                yield x
+            finally:
+                self._history.append("finish")
+
+        self._manager = manager
+
+    def test_context_manager(self):
+        with self._manager(123) as x:
+            self.assertEqual(x, 123)
+        self.assertEqual(self._history, ["start", "finish"])
+
+    def test_context_manager_on_error(self):
+        exc = Exception()
+        try:
+            with self._manager(123):
+                raise exc
+        except Exception as e:
+            self.assertEqual(exc, e)
+        self.assertEqual(self._history, ["start", "finish"])
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/ucontextlib/ucontextlib.py b/micropython/ucontextlib/ucontextlib.py
similarity index 99%
rename from ucontextlib/ucontextlib.py
rename to micropython/ucontextlib/ucontextlib.py
index 29445a028..d259f9b8f 100644
--- a/ucontextlib/ucontextlib.py
+++ b/micropython/ucontextlib/ucontextlib.py
@@ -9,6 +9,7 @@
  - supress
 """
 
+
 class ContextDecorator(object):
     "A base class or mixin that enables context managers to work as decorators."
 
@@ -28,6 +29,7 @@ def __call__(self, func):
         def inner(*args, **kwds):
             with self._recreate_cm():
                 return func(*args, **kwds)
+
         return inner
 
 
@@ -101,6 +103,8 @@ def some_generator(<arguments>):
             <cleanup>
 
     """
+
     def helper(*args, **kwds):
         return _GeneratorContextManager(func, *args, **kwds)
+
     return helper
diff --git a/micropython/udnspkt/example_resolve.py b/micropython/udnspkt/example_resolve.py
new file mode 100644
index 000000000..d72c17a48
--- /dev/null
+++ b/micropython/udnspkt/example_resolve.py
@@ -0,0 +1,29 @@
+import io
+import socket
+
+import udnspkt
+
+
+s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+dns_addr = socket.getaddrinfo("127.0.0.1", 53)[0][-1]
+
+
+def resolve(domain, is_ipv6):
+    buf = io.BytesIO(48)
+    udnspkt.make_req(buf, "google.com", is_ipv6)
+    v = buf.getvalue()
+    print("query: ", v)
+    s.sendto(v, dns_addr)
+
+    resp = s.recv(1024)
+    print("resp:", resp)
+    buf = io.BytesIO(resp)
+
+    addr = udnspkt.parse_resp(buf, is_ipv6)
+    print("bin addr:", addr)
+    print("addr:", socket.inet_ntop(socket.AF_INET6 if is_ipv6 else socket.AF_INET, addr))
+
+
+resolve("google.com", False)
+print()
+resolve("google.com", True)
diff --git a/micropython/udnspkt/manifest.py b/micropython/udnspkt/manifest.py
new file mode 100644
index 000000000..2c2a78d2b
--- /dev/null
+++ b/micropython/udnspkt/manifest.py
@@ -0,0 +1,5 @@
+metadata(description="Make and parse DNS packets (Sans I/O approach).", version="0.1.0")
+
+# Originally written by Paul Sokolovsky.
+
+module("udnspkt.py")
diff --git a/micropython/udnspkt/udnspkt.py b/micropython/udnspkt/udnspkt.py
new file mode 100644
index 000000000..f3b998a8a
--- /dev/null
+++ b/micropython/udnspkt/udnspkt.py
@@ -0,0 +1,67 @@
+def write_fqdn(buf, name):
+    parts = name.split(".")
+    for p in parts:
+        buf.writebin("B", len(p))
+        buf.write(p)
+    buf.writebin("B", 0)
+
+
+def skip_fqdn(buf):
+    while True:
+        sz = buf.readbin("B")
+        if not sz:
+            break
+        if sz >= 0xC0:
+            buf.readbin("B")
+            break
+        buf.read(sz)
+
+
+def make_req(buf, fqdn, is_ipv6):
+    typ = 1  # A
+    if is_ipv6:
+        typ = 28  # AAAA
+
+    buf.writebin(">H", 0)
+    buf.writebin(">H", 0x100)
+    # q count
+    buf.writebin(">H", 1)
+    buf.writebin(">H", 0)
+    # squashed together
+    buf.writebin(">I", 0)
+
+    write_fqdn(buf, fqdn)
+    buf.writebin(">H", typ)
+    buf.writebin(">H", 1)  # Class
+
+
+def parse_resp(buf, is_ipv6):
+    typ = 1  # A
+    if is_ipv6:
+        typ = 28  # AAAA
+
+    buf.readbin(">H")  # id
+    flags = buf.readbin(">H")
+    assert flags & 0x8000
+    buf.readbin(">H")  # qcnt
+    acnt = buf.readbin(">H")
+    buf.readbin(">H")  # nscnt
+    buf.readbin(">H")  # addcnt
+
+    skip_fqdn(buf)
+    buf.readbin(">H")
+    buf.readbin(">H")
+
+    for i in range(acnt):
+        # print("Resp #%d" % i)
+        # v = read_fqdn(buf)
+        # print(v)
+        skip_fqdn(buf)
+        t = buf.readbin(">H")  # Type
+        buf.readbin(">H")  # Class
+        buf.readbin(">I")  # TTL
+        rlen = buf.readbin(">H")
+        rval = buf.read(rlen)
+
+        if t == typ:
+            return rval
diff --git a/micropython/umqtt.robust/README.rst b/micropython/umqtt.robust/README.rst
new file mode 100644
index 000000000..4c7226d22
--- /dev/null
+++ b/micropython/umqtt.robust/README.rst
@@ -0,0 +1,110 @@
+umqtt.robust
+============
+
+umqtt is a simple MQTT client for MicroPython. (Note that it uses some
+MicroPython shortcuts and doesn't work with CPython). It consists of
+two submodules: umqtt.simple and umqtt.robust. umqtt.robust is built
+on top of umqtt.simple and adds auto-reconnect facilities for some of
+networking errors.
+
+What does it mean to be "robust" ?
+----------------------------------
+
+Modern computing systems are sufficiently complex and have multiple
+points of failure. Consider for example that nothing will work if
+there's no power (mains outage or battery ran out). As you may imagine,
+umqtt.robust won't help you with your flat battery. Most computing
+systems are now networked, and communication is another weak link.
+This is especially true for wireless communications. If two of your
+systems can't connect reliably communicate via WiFi, umqtt.robust
+can't magically resolve that (but it may help with intermittent
+WiFi issues).
+
+What umqtt.robust tries to do is very simple - if while trying to
+perform some operation, it detects that connection to MQTT breaks,
+it tries to reconnect to it. That's good direction towards "robustness",
+but the problem that there is no single definition of what "robust"
+is. Let's consider following usecase:
+
+1. A temperature reading gets transmitted once a minute. Then the
+best option in case of a transmission error might be not doing
+anything at all - in a minute, another reading will be transmitted,
+and for slowly-changing parameter like a temperature, a one-minute
+lost reading is not a problem. Actually, if the sending device is
+battery-powered, any connection retries will just drain battery and
+make device "less robust" (it will run out of juice sooner and more
+unexpectedly, which may be a criteria for "robustness").
+
+2. If there's a button, which communicates its press event, then
+perhaps it's really worth to retry to deliver this event (a user
+expects something to happen when they press the button, right?).
+But if a button is battery-power, unconstrained retries won't do
+much good still. Consider mains power outage for several hours,
+MQTT server down all this time, and battery-powered button trying
+to re-publish event every second. It will likely drain battery
+during this time, which is very non-robust. Perhaps, if a press
+isn't delivered in 15 seconds, it's no longer relevant (depending
+on what press does, the above may be good for a button turning
+on lights, but not for something else!)
+
+3. Finally, let's consider security sensors, like a window broken
+sensor. That's the hardest case. Apparently, those events are
+important enough to be delivered no matter what. But if done with
+short, dumb retries, it will only lead to quick battery drain. So,
+a robust device would retry, but in smart manner, to let battery
+run for as long as possible, to maximize the chance of the message
+being delivered.
+
+Let's sum it up:
+
+a) There's no single definition of what "robust" is. It depends on
+   a particular application.
+b) Robustness is a complex measure, it doesn't depend on one single
+   feature, but rather many different features working together.
+   Consider for example that to make button from the case 2 above
+   work better, it would help to add a visual feedback, so a user
+   knew what happens.
+
+As you may imagine, umqtt.robust doesn't, and can't, cover all possible
+"robustness" scenarios, nor it alone can make your MQTT application
+"robust". Rather, it's a barebones example of how to reconnect to an
+MQTT server in case of a connection error. As such, it's just one
+of many steps required to make your app robust, and majority of those
+steps lie on *your application* side. With that in mind, any realistic
+application would subclass umqtt.robust.MQTTClient class and override
+delay() and reconnect() methods to suit particular usage scenario. It
+may even happen that umqtt.robust won't even suit your needs, and you
+will need to implement your "robust" handling from scratch.
+
+
+Persistent and non-persistent MQTT servers
+------------------------------------------
+
+Consider an example: you subscribed to some MQTT topics, then connection
+went down. If we talk "robust", then once you reconnect, you want any
+messages which arrived when the connection was down, to be still delivered
+to you. That requires retainment and persistency enabled on MQTT server.
+As umqtt.robust tries to achieve as much "robustness" as possible, it
+makes a requirement that the MQTT server it communicates to has persistency
+enabled. This include persistent sessions, meaning that any client
+subscriptions are retained across disconnect, and if you subscribed once,
+you no longer need to resubscribe again on next connection(s). This makes
+it more robust, minimizing amount of traffic to transfer on each connection
+(the more you transfer, the higher probability of error), and also saves
+battery power.
+
+However, not all broker offer true, persistent MQTT support:
+
+* If you use self-hosted broker, you may need to configure it for
+  persistency. E.g., a popular open-source broker Mosquitto requires
+  following line::
+
+    persistence true
+
+  to be added to ``mosquitto.conf``. Please consult documentation of
+  your broker.
+
+* Many so-called "cloud providers" offer very limited subset of MQTT for
+  their free/inexpensive tiers. Persistence and QoS are features usually
+  not supported. It's hard to achieve any true robustness with these
+  demo-like offerings, and umqtt.robust isn't designed to work with them.
diff --git a/micropython/umqtt.robust/example_check_msg_robust.py b/micropython/umqtt.robust/example_check_msg_robust.py
new file mode 100644
index 000000000..2374489c6
--- /dev/null
+++ b/micropython/umqtt.robust/example_check_msg_robust.py
@@ -0,0 +1,14 @@
+import umqtt.robust
+import time
+
+# Instantiate an MQTTClient with a keepalive time of 5 seconds (to help us test
+# what happens to check_msg() with a broken connection)
+m = umqtt.robust.MQTTClient(host="localhost", debug=True, keepalive=5)
+
+m.connect()
+
+# Wait for the broker to consider us dead
+time.sleep(6)
+
+# This should initiate a reconnect() and return immediately
+m.check_msg()
diff --git a/micropython/umqtt.robust/example_sub_robust.py b/micropython/umqtt.robust/example_sub_robust.py
new file mode 100644
index 000000000..f09befe02
--- /dev/null
+++ b/micropython/umqtt.robust/example_sub_robust.py
@@ -0,0 +1,40 @@
+import time
+from umqtt.robust import MQTTClient
+
+
+def sub_cb(topic, msg):
+    print((topic, msg))
+
+
+c = MQTTClient("umqtt_client", "localhost")
+# Print diagnostic messages when retries/reconnects happens
+c.DEBUG = True
+c.set_callback(sub_cb)
+# Connect to server, requesting not to clean session for this
+# client. If there was no existing session (False return value
+# from connect() method), we perform the initial setup of client
+# session - subscribe to needed topics. Afterwards, these
+# subscriptions will be stored server-side, and will be persistent,
+# (as we use clean_session=False).
+#
+# There can be a problem when a session for a given client exists,
+# but doesn't have subscriptions a particular application expects.
+# In this case, a session needs to be cleaned first.
+#
+# In an actual application, it's up to its developer how to
+# manage these issues. One extreme is to have external "provisioning"
+# phase, where initial session setup, and any further management of
+# a session, is done by external tools. This allows to save resources
+# on a small embedded device. Another extreme is to have an application
+# to perform auto-setup (e.g., clean session, then re-create session
+# on each restart). This example shows mid-line between these 2
+# approaches, where initial setup of session is done by application,
+# but if anything goes wrong, there's an external tool to clean session.
+if not c.connect(clean_session=False):
+    print("New session being set up")
+    c.subscribe(b"foo_topic")
+
+while 1:
+    c.wait_msg()
+
+c.disconnect()
diff --git a/micropython/umqtt.robust/manifest.py b/micropython/umqtt.robust/manifest.py
new file mode 100644
index 000000000..fe388b415
--- /dev/null
+++ b/micropython/umqtt.robust/manifest.py
@@ -0,0 +1,7 @@
+metadata(
+    description='Lightweight MQTT client for MicroPython ("robust" version).', version="1.0.2"
+)
+
+# Originally written by Paul Sokolovsky.
+
+package("umqtt")
diff --git a/micropython/umqtt.robust/umqtt/robust.py b/micropython/umqtt.robust/umqtt/robust.py
new file mode 100644
index 000000000..51596de9e
--- /dev/null
+++ b/micropython/umqtt.robust/umqtt/robust.py
@@ -0,0 +1,53 @@
+import time
+from . import simple
+
+
+class MQTTClient(simple.MQTTClient):
+    DELAY = 2
+    DEBUG = False
+
+    def delay(self, i):
+        time.sleep(self.DELAY)
+
+    def log(self, in_reconnect, e):
+        if self.DEBUG:
+            if in_reconnect:
+                print("mqtt reconnect: %r" % e)
+            else:
+                print("mqtt: %r" % e)
+
+    def reconnect(self):
+        i = 0
+        while 1:
+            try:
+                return super().connect(False)
+            except OSError as e:
+                self.log(True, e)
+                i += 1
+                self.delay(i)
+
+    def publish(self, topic, msg, retain=False, qos=0):
+        while 1:
+            try:
+                return super().publish(topic, msg, retain, qos)
+            except OSError as e:
+                self.log(False, e)
+            self.reconnect()
+
+    def wait_msg(self):
+        while 1:
+            try:
+                return super().wait_msg()
+            except OSError as e:
+                self.log(False, e)
+            self.reconnect()
+
+    def check_msg(self, attempts=2):
+        while attempts:
+            self.sock.setblocking(False)
+            try:
+                return super().wait_msg()
+            except OSError as e:
+                self.log(False, e)
+            self.reconnect()
+            attempts -= 1
diff --git a/micropython/umqtt.simple/README.rst b/micropython/umqtt.simple/README.rst
new file mode 100644
index 000000000..d9d09b970
--- /dev/null
+++ b/micropython/umqtt.simple/README.rst
@@ -0,0 +1,91 @@
+umqtt.simple
+============
+
+umqtt is a simple MQTT client for MicroPython. (Note that it uses some
+MicroPython shortcuts and doesn't work with CPython).
+
+Design requirements
+-------------------
+
+* Memory efficiency.
+* Avoid infamous design anti-patterns like "callback hell".
+* Support for both publishing and subscription via a single client
+  object (another alternative would be to have separate client classes
+  for publishing and subscription).
+
+API design
+----------
+
+Based on the requirements above, there are following API traits:
+
+* All data related to MQTT messages is encoded as bytes. This includes
+  both message content AND topic names (even though MQTT spec states
+  that topic name is UTF-8 encoded). The reason for this is simple:
+  what is received over network socket is binary data (bytes) and
+  it would require extra step to convert that to a string, spending
+  memory on that. Note that this applies only to topic names (because
+  they can be both sent and received). Other parameters specified by
+  MQTT as UTF-8 encoded (e.g. ClientID) are accepted as strings.
+* Subscribed messages are delivered via a callback. This is to avoid
+  using a queue for subscribed messages, as otherwise they may be
+  received at any time (including when client expects other type
+  of server response, so there're 2 choices: either deliver them
+  immediately via a callback or queue up until an "expected" response
+  arrives). Note that lack of need for a queue is delusive: the
+  runtime call stack forms an implicit queue in this case. And unlike
+  explicit queue, it's much harder to control. This design was chosen
+  because in a common case of processing subscribed messages it's
+  the most efficient. However, if in subscription callback, new
+  messages of QoS>0 are published, this may lead to deep, or
+  infinite recursion (the latter means an application will terminate
+  with ``RuntimeException``).
+
+API reference
+-------------
+
+Taking into account API traits described above, umqtt pretty closely
+follows MQTT control operations, and maps them to class methods:
+
+* ``connect(...)`` - Connect to a server. Returns True if this connection
+  uses persisten session stored on a server (this will be always False if
+  clean_session=True argument is used (default)).
+* ``disconnect()`` - Disconnect from a server, release resources.
+* ``ping()`` - Ping server (response is processed automatically by wait_msg()).
+* ``publish()`` - Publish a message.
+* ``subscribe()`` - Subscribe to a topic.
+* ``set_callback()`` - Set callback for received subscription messages.
+* ``set_last_will()`` - Set MQTT "last will" message. Should be called
+  *before* connect().
+* ``wait_msg()`` - Wait for a server message. A subscription message will be
+  delivered to a callback set with set_callback(), any other messages
+  will be processed internally.
+* ``check_msg()`` - Check if there's pending message from server. If yes,
+  process the same way as wait_msg(), if not, return immediately.
+
+``wait_msg()`` and ``check_msg()`` are "main loop iteration" methods, blocking
+and non-blocking version. They should be called periodically in a loop,
+``wait_msg()`` if you don't have any other foreground tasks to perform
+(i.e. your app just reacts to subscribed MQTT messages), ``check_msg()``
+if you process other foreground tasks too.
+
+Note that you don't need to call ``wait_msg()``/``check_msg()`` if you only
+publish messages, never subscribe to them.
+
+For more detailed information about API please see the source code
+(which is quite short and easy to review) and provided examples.
+
+
+Supported MQTT features
+-----------------------
+
+QoS 0 and 1 are supported for both publish and subscribe. QoS2 isn't
+supported to keep code size small. Besides ClientID, only "clean
+session" parameter is supported for connect as of now.
+
+
+MQTT client with automatic reconnect
+------------------------------------
+
+There's a separate `umqtt.robust` module which builds on `umqtt.simple`
+and adds automatic reconnect support in case of network errors.
+Please see its documentation for further details.
diff --git a/micropython/umqtt.simple/example_pub.py b/micropython/umqtt.simple/example_pub.py
new file mode 100644
index 000000000..082069cd4
--- /dev/null
+++ b/micropython/umqtt.simple/example_pub.py
@@ -0,0 +1,15 @@
+from umqtt.simple import MQTTClient
+
+# Test reception e.g. with:
+# mosquitto_sub -t foo_topic
+
+
+def main(server="localhost"):
+    c = MQTTClient("umqtt_client", server)
+    c.connect()
+    c.publish(b"foo_topic", b"hello")
+    c.disconnect()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/micropython/umqtt.simple/example_pub_button.py b/micropython/umqtt.simple/example_pub_button.py
new file mode 100644
index 000000000..2a3ec851e
--- /dev/null
+++ b/micropython/umqtt.simple/example_pub_button.py
@@ -0,0 +1,30 @@
+import time
+import binascii
+import machine
+from umqtt.simple import MQTTClient
+from machine import Pin
+
+
+# Many ESP8266 boards have active-low "flash" button on GPIO0.
+button = Pin(0, Pin.IN)
+
+# Default MQTT server to connect to
+SERVER = "192.168.1.35"
+CLIENT_ID = binascii.hexlify(machine.unique_id())
+TOPIC = b"led"
+
+
+def main(server=SERVER):
+    c = MQTTClient(CLIENT_ID, server)
+    c.connect()
+    print("Connected to %s, waiting for button presses" % server)
+    while True:
+        while True:
+            if button.value() == 0:
+                break
+            time.sleep_ms(20)
+        print("Button pressed")
+        c.publish(TOPIC, b"toggle")
+        time.sleep_ms(200)
+
+    c.disconnect()
diff --git a/micropython/umqtt.simple/example_sub.py b/micropython/umqtt.simple/example_sub.py
new file mode 100644
index 000000000..41fc55bcf
--- /dev/null
+++ b/micropython/umqtt.simple/example_sub.py
@@ -0,0 +1,33 @@
+import time
+from umqtt.simple import MQTTClient
+
+# Publish test messages e.g. with:
+# mosquitto_pub -t foo_topic -m hello
+
+
+# Received messages from subscriptions will be delivered to this callback
+def sub_cb(topic, msg):
+    print((topic, msg))
+
+
+def main(server="localhost"):
+    c = MQTTClient("umqtt_client", server)
+    c.set_callback(sub_cb)
+    c.connect()
+    c.subscribe(b"foo_topic")
+    while True:
+        if True:
+            # Blocking wait for message
+            c.wait_msg()
+        else:
+            # Non-blocking wait for message
+            c.check_msg()
+            # Then need to sleep to avoid 100% CPU usage (in a real
+            # app other useful actions would be performed instead)
+            time.sleep(1)
+
+    c.disconnect()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/micropython/umqtt.simple/example_sub_led.py b/micropython/umqtt.simple/example_sub_led.py
new file mode 100644
index 000000000..c3dcf08d2
--- /dev/null
+++ b/micropython/umqtt.simple/example_sub_led.py
@@ -0,0 +1,50 @@
+from umqtt.simple import MQTTClient
+from machine import Pin
+import binascii
+import machine
+import micropython
+
+
+# ESP8266 ESP-12 modules have blue, active-low LED on GPIO2, replace
+# with something else if needed.
+led = Pin(2, Pin.OUT, value=1)
+
+# Default MQTT server to connect to
+SERVER = "192.168.1.35"
+CLIENT_ID = binascii.hexlify(machine.unique_id())
+TOPIC = b"led"
+
+
+state = 0
+
+
+def sub_cb(topic, msg):
+    global state
+    print((topic, msg))
+    if msg == b"on":
+        led.value(0)
+        state = 1
+    elif msg == b"off":
+        led.value(1)
+        state = 0
+    elif msg == b"toggle":
+        # LED is inversed, so setting it to current state
+        # value will make it toggle
+        led.value(state)
+        state = 1 - state
+
+
+def main(server=SERVER):
+    c = MQTTClient(CLIENT_ID, server)
+    # Subscribed messages will be delivered to this callback
+    c.set_callback(sub_cb)
+    c.connect()
+    c.subscribe(TOPIC)
+    print("Connected to %s, subscribed to %s topic" % (server, TOPIC))
+
+    try:
+        while 1:
+            # micropython.mem_info()
+            c.wait_msg()
+    finally:
+        c.disconnect()
diff --git a/micropython/umqtt.simple/manifest.py b/micropython/umqtt.simple/manifest.py
new file mode 100644
index 000000000..709a27505
--- /dev/null
+++ b/micropython/umqtt.simple/manifest.py
@@ -0,0 +1,7 @@
+metadata(description="Lightweight MQTT client for MicroPython.", version="1.6.0")
+
+# Originally written by Paul Sokolovsky.
+
+require("ssl")
+
+package("umqtt")
diff --git a/micropython/umqtt.simple/umqtt/simple.py b/micropython/umqtt.simple/umqtt/simple.py
new file mode 100644
index 000000000..d9cdffc47
--- /dev/null
+++ b/micropython/umqtt.simple/umqtt/simple.py
@@ -0,0 +1,220 @@
+import socket
+import struct
+from binascii import hexlify
+
+
+class MQTTException(Exception):
+    pass
+
+
+class MQTTClient:
+    def __init__(
+        self,
+        client_id,
+        server,
+        port=0,
+        user=None,
+        password=None,
+        keepalive=0,
+        ssl=None,
+        ssl_params={},
+    ):
+        if port == 0:
+            port = 8883 if ssl else 1883
+        self.client_id = client_id
+        self.sock = None
+        self.server = server
+        self.port = port
+        self.ssl = ssl
+        self.ssl_params = ssl_params
+        self.pid = 0
+        self.cb = None
+        self.user = user
+        self.pswd = password
+        self.keepalive = keepalive
+        self.lw_topic = None
+        self.lw_msg = None
+        self.lw_qos = 0
+        self.lw_retain = False
+
+    def _send_str(self, s):
+        self.sock.write(struct.pack("!H", len(s)))
+        self.sock.write(s)
+
+    def _recv_len(self):
+        n = 0
+        sh = 0
+        while 1:
+            b = self.sock.read(1)[0]
+            n |= (b & 0x7F) << sh
+            if not b & 0x80:
+                return n
+            sh += 7
+
+    def set_callback(self, f):
+        self.cb = f
+
+    def set_last_will(self, topic, msg, retain=False, qos=0):
+        assert 0 <= qos <= 2
+        assert topic
+        self.lw_topic = topic
+        self.lw_msg = msg
+        self.lw_qos = qos
+        self.lw_retain = retain
+
+    def connect(self, clean_session=True, timeout=None):
+        self.sock = socket.socket()
+        self.sock.settimeout(timeout)
+        addr = socket.getaddrinfo(self.server, self.port)[0][-1]
+        self.sock.connect(addr)
+        if self.ssl is True:
+            # Legacy support for ssl=True and ssl_params arguments.
+            import ssl
+
+            self.sock = ssl.wrap_socket(self.sock, **self.ssl_params)
+        elif self.ssl:
+            self.sock = self.ssl.wrap_socket(self.sock, server_hostname=self.server)
+        premsg = bytearray(b"\x10\0\0\0\0\0")
+        msg = bytearray(b"\x04MQTT\x04\x02\0\0")
+
+        sz = 10 + 2 + len(self.client_id)
+        msg[6] = clean_session << 1
+        if self.user:
+            sz += 2 + len(self.user) + 2 + len(self.pswd)
+            msg[6] |= 0xC0
+        if self.keepalive:
+            assert self.keepalive < 65536
+            msg[7] |= self.keepalive >> 8
+            msg[8] |= self.keepalive & 0x00FF
+        if self.lw_topic:
+            sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
+            msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
+            msg[6] |= self.lw_retain << 5
+
+        i = 1
+        while sz > 0x7F:
+            premsg[i] = (sz & 0x7F) | 0x80
+            sz >>= 7
+            i += 1
+        premsg[i] = sz
+
+        self.sock.write(premsg, i + 2)
+        self.sock.write(msg)
+        # print(hex(len(msg)), hexlify(msg, ":"))
+        self._send_str(self.client_id)
+        if self.lw_topic:
+            self._send_str(self.lw_topic)
+            self._send_str(self.lw_msg)
+        if self.user:
+            self._send_str(self.user)
+            self._send_str(self.pswd)
+        resp = self.sock.read(4)
+        assert resp[0] == 0x20 and resp[1] == 0x02
+        if resp[3] != 0:
+            raise MQTTException(resp[3])
+        return resp[2] & 1
+
+    def disconnect(self):
+        self.sock.write(b"\xe0\0")
+        self.sock.close()
+
+    def ping(self):
+        self.sock.write(b"\xc0\0")
+
+    def publish(self, topic, msg, retain=False, qos=0):
+        pkt = bytearray(b"\x30\0\0\0")
+        pkt[0] |= qos << 1 | retain
+        sz = 2 + len(topic) + len(msg)
+        if qos > 0:
+            sz += 2
+        assert sz < 2097152
+        i = 1
+        while sz > 0x7F:
+            pkt[i] = (sz & 0x7F) | 0x80
+            sz >>= 7
+            i += 1
+        pkt[i] = sz
+        # print(hex(len(pkt)), hexlify(pkt, ":"))
+        self.sock.write(pkt, i + 1)
+        self._send_str(topic)
+        if qos > 0:
+            self.pid += 1
+            pid = self.pid
+            struct.pack_into("!H", pkt, 0, pid)
+            self.sock.write(pkt, 2)
+        self.sock.write(msg)
+        if qos == 1:
+            while 1:
+                op = self.wait_msg()
+                if op == 0x40:
+                    sz = self.sock.read(1)
+                    assert sz == b"\x02"
+                    rcv_pid = self.sock.read(2)
+                    rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
+                    if pid == rcv_pid:
+                        return
+        elif qos == 2:
+            assert 0
+
+    def subscribe(self, topic, qos=0):
+        assert self.cb is not None, "Subscribe callback is not set"
+        pkt = bytearray(b"\x82\0\0\0")
+        self.pid += 1
+        struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
+        # print(hex(len(pkt)), hexlify(pkt, ":"))
+        self.sock.write(pkt)
+        self._send_str(topic)
+        self.sock.write(qos.to_bytes(1, "little"))
+        while 1:
+            op = self.wait_msg()
+            if op == 0x90:
+                resp = self.sock.read(4)
+                # print(resp)
+                assert resp[1] == pkt[2] and resp[2] == pkt[3]
+                if resp[3] == 0x80:
+                    raise MQTTException(resp[3])
+                return
+
+    # Wait for a single incoming MQTT message and process it.
+    # Subscribed messages are delivered to a callback previously
+    # set by .set_callback() method. Other (internal) MQTT
+    # messages processed internally.
+    def wait_msg(self):
+        res = self.sock.read(1)
+        self.sock.setblocking(True)
+        if res is None:
+            return None
+        if res == b"":
+            raise OSError(-1)
+        if res == b"\xd0":  # PINGRESP
+            sz = self.sock.read(1)[0]
+            assert sz == 0
+            return None
+        op = res[0]
+        if op & 0xF0 != 0x30:
+            return op
+        sz = self._recv_len()
+        topic_len = self.sock.read(2)
+        topic_len = (topic_len[0] << 8) | topic_len[1]
+        topic = self.sock.read(topic_len)
+        sz -= topic_len + 2
+        if op & 6:
+            pid = self.sock.read(2)
+            pid = pid[0] << 8 | pid[1]
+            sz -= 2
+        msg = self.sock.read(sz)
+        self.cb(topic, msg)
+        if op & 6 == 2:
+            pkt = bytearray(b"\x40\x02\0\0")
+            struct.pack_into("!H", pkt, 2, pid)
+            self.sock.write(pkt)
+        elif op & 6 == 4:
+            assert 0
+        return op
+
+    # Checks whether a pending message from server is available.
+    # If not, returns immediately with None. Otherwise, does
+    # the same processing as wait_msg.
+    def check_msg(self):
+        self.sock.setblocking(False)
+        return self.wait_msg()
diff --git a/micropython/upysh/manifest.py b/micropython/upysh/manifest.py
new file mode 100644
index 000000000..2d36a9ab4
--- /dev/null
+++ b/micropython/upysh/manifest.py
@@ -0,0 +1,3 @@
+metadata(description="Minimalistic file shell using native Python syntax.", version="0.6.1")
+
+module("upysh.py")
diff --git a/micropython/upysh/upysh.py b/micropython/upysh/upysh.py
new file mode 100644
index 000000000..0f0ad65ba
--- /dev/null
+++ b/micropython/upysh/upysh.py
@@ -0,0 +1,124 @@
+import sys
+import os
+
+
+class LS:
+    def __repr__(self):
+        self.__call__()
+        return ""
+
+    def __call__(self, path="."):
+        l = list(os.ilistdir(path))
+        l.sort()
+        for f in l:
+            if f[1] == 0x4000:  # stat.S_IFDIR
+                print("    <dir> %s" % f[0])
+        for f in l:
+            if f[1] != 0x4000:
+                if len(f) > 3:
+                    print("% 9d %s" % (f[3], f[0]))
+                else:
+                    print("          %s" % f[0])
+        try:
+            st = os.statvfs(path)
+            print("\n{:,d}k free".format(st[1] * st[3] // 1024))
+        except:
+            pass
+
+
+class PWD:
+    def __repr__(self):
+        return os.getcwd()
+
+    def __call__(self):
+        return self.__repr__()
+
+
+class CLEAR:
+    def __repr__(self):
+        return "\x1b[2J\x1b[H"
+
+    def __call__(self):
+        return self.__repr__()
+
+
+def head(f, n=10):
+    with open(f) as f:
+        for i in range(n):
+            l = f.readline()
+            if not l:
+                break
+            sys.stdout.write(l)
+
+
+def cat(f):
+    head(f, 1 << 30)
+
+
+def cp(s, t):
+    try:
+        if os.stat(t)[0] & 0x4000:  # is directory
+            t = t.rstrip("/") + "/" + s
+    except OSError:
+        pass
+    buf = bytearray(512)
+    buf_mv = memoryview(buf)
+    with open(s, "rb") as s, open(t, "wb") as t:
+        while True:
+            n = s.readinto(buf)
+            if n <= 0:
+                break
+            t.write(buf_mv[:n])
+
+
+def newfile(path):
+    print("Type file contents line by line, finish with EOF (Ctrl+D).")
+    with open(path, "w") as f:
+        while 1:
+            try:
+                l = input()
+            except EOFError:
+                break
+            f.write(l)
+            f.write("\n")
+
+
+def rm(d, recursive=False):  # Remove file or tree
+    try:
+        if (os.stat(d)[0] & 0x4000) and recursive:  # Dir
+            for f in os.ilistdir(d):
+                if f[0] != "." and f[0] != "..":
+                    rm("/".join((d, f[0])))  # File or Dir
+            os.rmdir(d)
+        else:  # File
+            os.remove(d)
+    except:
+        print("rm of '%s' failed" % d)
+
+
+class Man:
+    def __repr__(self):
+        return """
+upysh is intended to be imported using:
+from upysh import *
+
+To see this help text again, type "man".
+
+upysh commands:
+clear, ls, ls(...), head(...), cat(...), newfile(...)
+cp('src', 'dest'), mv('old', 'new'), rm(...)
+pwd, cd(...), mkdir(...), rmdir(...)
+"""
+
+
+man = Man()
+pwd = PWD()
+ls = LS()
+clear = CLEAR()
+
+cd = os.chdir
+mkdir = os.mkdir
+mv = os.rename
+rmdir = os.rmdir
+
+print(man)
diff --git a/micropython/urequests/README.md b/micropython/urequests/README.md
new file mode 100644
index 000000000..f6612b356
--- /dev/null
+++ b/micropython/urequests/README.md
@@ -0,0 +1,9 @@
+## urequests compatibility
+
+The MicroPython version of
+[requests](https://requests.readthedocs.io/en/latest/) was previously called
+`urequests` and a lot of existing code depends on being able to still
+import the module by that name.
+
+This package provides a wrapper to allow this. Prefer to install and use the
+`requests` package instead.
diff --git a/micropython/urequests/manifest.py b/micropython/urequests/manifest.py
new file mode 100644
index 000000000..3fbe61c25
--- /dev/null
+++ b/micropython/urequests/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.8.0", pypi="requests")
+
+require("requests")
+
+module("urequests.py")
diff --git a/micropython/urequests/urequests.py b/micropython/urequests/urequests.py
new file mode 100644
index 000000000..227a1ae5c
--- /dev/null
+++ b/micropython/urequests/urequests.py
@@ -0,0 +1,8 @@
+# This module provides a backwards-compatble import for `urequests`.
+# It lazy-loads from `requests` without duplicating its globals dict.
+
+
+def __getattr__(attr):
+    import requests
+
+    return getattr(requests, attr)
diff --git a/micropython/urllib.urequest/manifest.py b/micropython/urllib.urequest/manifest.py
new file mode 100644
index 000000000..033022711
--- /dev/null
+++ b/micropython/urllib.urequest/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.8.0")
+
+# Originally written by Paul Sokolovsky.
+
+package("urllib")
diff --git a/micropython/urllib.urequest/urllib/urequest.py b/micropython/urllib.urequest/urllib/urequest.py
new file mode 100644
index 000000000..a9622ea89
--- /dev/null
+++ b/micropython/urllib.urequest/urllib/urequest.py
@@ -0,0 +1,75 @@
+import socket
+
+
+def urlopen(url, data=None, method="GET", headers={}):
+    if data is not None and method == "GET":
+        method = "POST"
+    try:
+        proto, dummy, host, path = url.split("/", 3)
+    except ValueError:
+        proto, dummy, host = url.split("/", 2)
+        path = ""
+    if proto == "http:":
+        port = 80
+    elif proto == "https:":
+        import tls
+
+        port = 443
+    else:
+        raise ValueError("Unsupported protocol: " + proto)
+
+    if ":" in host:
+        host, port = host.split(":", 1)
+        port = int(port)
+
+    ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
+    ai = ai[0]
+
+    s = socket.socket(ai[0], ai[1], ai[2])
+    try:
+        s.connect(ai[-1])
+        if proto == "https:":
+            context = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+            context.verify_mode = tls.CERT_NONE
+            s = context.wrap_socket(s, server_hostname=host)
+
+        s.write(method)
+        s.write(b" /")
+        s.write(path)
+        s.write(b" HTTP/1.0\r\nHost: ")
+        s.write(host)
+        s.write(b"\r\n")
+
+        for k in headers:
+            s.write(k)
+            s.write(b": ")
+            s.write(headers[k])
+            s.write(b"\r\n")
+
+        if data:
+            s.write(b"Content-Length: ")
+            s.write(str(len(data)))
+            s.write(b"\r\n")
+        s.write(b"\r\n")
+        if data:
+            s.write(data)
+
+        l = s.readline()  # Status-Line
+        # l = l.split(None, 2)
+        # print(l)
+        # status = int(l[1])  # FIXME: Status-Code element is not currently checked
+        while True:
+            l = s.readline()
+            if not l or l == b"\r\n":
+                break
+            # print(l)
+            if l.startswith(b"Transfer-Encoding:"):
+                if b"chunked" in l:
+                    raise ValueError("Unsupported " + l)
+            elif l.startswith(b"Location:"):
+                raise NotImplementedError("Redirects not yet supported")
+    except OSError:
+        s.close()
+        raise
+
+    return s
diff --git a/micropython/usb/README.md b/micropython/usb/README.md
new file mode 100644
index 000000000..d4b975d12
--- /dev/null
+++ b/micropython/usb/README.md
@@ -0,0 +1,148 @@
+# USB Drivers
+
+These packages allow implementing USB functionality on a MicroPython system
+using pure Python code.
+
+Currently only USB device is implemented, not USB host.
+
+## USB Device support
+
+### Support
+
+USB Device support depends on the low-level
+[machine.USBDevice](https://docs.micropython.org/en/latest/library/machine.USBDevice.html)
+class. This class is new and not supported on all ports, so please check the
+documentation for your MicroPython version. It is possible to implement a USB
+device using only the low-level USBDevice class. However, the packages here are
+higher level and easier to use.
+
+For more information about how to install packages, or "freeze" them into a
+firmware image, consult the [MicroPython documentation on "Package
+management"](https://docs.micropython.org/en/latest/reference/packages.html).
+
+### Examples
+
+The [examples/device](examples/device) directory in this repo has a range of
+examples. After installing necessary packages, you can download an example and
+run it with `mpremote run EXAMPLE_FILENAME.py` ([mpremote
+docs](https://docs.micropython.org/en/latest/reference/mpremote.html#mpremote-command-run)).
+
+#### Unexpected serial disconnects
+
+If you normally connect to your MicroPython device over a USB serial port ("USB
+CDC"), then running a USB example will disconnect mpremote when the new USB
+device configuration activates and the serial port has to temporarily
+disconnect. It is likely that mpremote will print an error. The example should
+still start running, if necessary then you can reconnect with mpremote and type
+Ctrl-B to restore the MicroPython REPL and/or Ctrl-C to stop the running
+example.
+
+If you use `mpremote run` again while a different USB device configuration is
+already active, then the USB serial port may disconnect immediately before the
+example runs. This is because mpremote has to soft-reset MicroPython, and when
+the existing USB device is reset then the entire USB port needs to reset. If
+this happens, run the same `mpremote run` command again.
+
+We plan to add features to `mpremote` so that this limitation is less
+disruptive. Other tools that communicate with MicroPython over the serial port
+will encounter similar issues when runtime USB is in use.
+
+### Initialising runtime USB
+
+The overall pattern for enabling USB devices at runtime is:
+
+1. Instantiate the Interface objects for your desired USB device.
+2. Call `usb.device.get()` to get the singleton object for the high-level USB device.
+3. Call `init(...)` to pass the desired interfaces as arguments, plus any custom
+   keyword arguments to configure the overall device.
+
+An example, similar to [mouse_example.py](examples/device/mouse_example.py):
+
+```py
+    m = usb.device.mouse.MouseInterface()
+    usb.device.get().init(m, builtin_driver=True)
+```
+
+Setting `builtin_driver=True` means that any built-in USB serial port will still
+be available. Otherwise, you may permanently lose access to MicroPython until
+the next time the device resets.
+
+See [Unexpected serial disconnects](#Unexpected-serial-disconnects), above, for
+an explanation of possible errors or disconnects when the runtime USB device
+initialises.
+
+Placing the call to `usb.device.get().init()` into the `boot.py` of the
+MicroPython file system allows the runtime USB device to initialise immediately
+on boot, before any built-in USB. This is a feature (not a bug) and allows you
+full control over the USB device, for example to only enable USB HID and prevent
+REPL access to the system.
+
+However, note that calling this function on boot without `builtin_driver=True`
+will make the MicroPython USB serial interface permanently inaccessible until
+you "safe mode boot" (on supported boards) or completely erase the flash of your
+device.
+
+### Package usb-device
+
+This base package contains the common implementation components for the other
+packages, and can be used to implement new and different USB interface support.
+All of the other `usb-device-<name>` packages depend on this package, and it
+will be automatically installed as needed.
+
+Specicially, this package provides the `usb.device.get()` function for accessing
+the Device singleton object, and the `usb.device.core` module which contains the
+low-level classes and utility functions for implementing new USB interface
+drivers in Python. The best examples of how to use the core classes is the
+source code of the other USB device packages.
+
+### Package usb-device-keyboard
+
+This package provides the `usb.device.keyboard` module. See
+[keyboard_example.py](examples/device/keyboard_example.py) for an example
+program.
+
+### Package usb-device-mouse
+
+This package provides the `usb.device.mouse` module. See
+[mouse_example.py](examples/device/mouse_example.py) for an example program.
+
+### Package usb-device-hid
+
+This package provides the `usb.device.hid` module. USB HID (Human Interface
+Device) class allows creating a wide variety of device types. The most common
+are mouse and keyboard, which have their own packages in micropython-lib.
+However, using the usb-device-hid package directly allows creation of any kind
+of HID device.
+
+See [hid_custom_keypad_example.py](examples/device/hid_custom_keypad_example.py)
+for an example of a Keypad HID device with a custom HID descriptor.
+
+### Package usb-device-cdc
+
+This package provides the `usb.device.cdc` module. USB CDC (Communications
+Device Class) is most commonly used for virtual serial port USB interfaces, and
+that is what is supported here.
+
+The example [cdc_repl_example.py](examples/device/cdc_repl_example.py)
+demonstrates how to add a second USB serial interface and duplicate the
+MicroPython REPL between the two.
+
+### Package usb-device-midi
+
+This package provides the `usb.device.midi` module. This allows implementing
+USB MIDI devices in MicroPython.
+
+The example [midi_example.py](examples/device/midi_example.py) demonstrates how
+to create a simple MIDI device to send MIDI data to and from the USB host.
+
+### Limitations
+
+#### Buffer thread safety
+
+The internal Buffer class that's used by most of the USB device classes expects data
+to be written to it (i.e. sent to the host) by only one thread. Bytes may be
+lost from the USB transfers if more than one thread (or a thread and a callback)
+try to write to the buffer simultaneously.
+
+If writing USB data from multiple sources, your code may need to add
+synchronisation (i.e. locks).
diff --git a/micropython/usb/examples/device/cdc_repl_example.py b/micropython/usb/examples/device/cdc_repl_example.py
new file mode 100644
index 000000000..06dc9a76c
--- /dev/null
+++ b/micropython/usb/examples/device/cdc_repl_example.py
@@ -0,0 +1,47 @@
+# MicroPython USB CDC REPL example
+#
+# Example demonstrating how to use os.dupterm() to provide the
+# MicroPython REPL on a dynamic CDCInterface() serial port.
+#
+# To run this example:
+#
+# 1. Make sure `usb-device-cdc` is installed via: mpremote mip install usb-device-cdc
+#
+# 2. Run the example via: mpremote run cdc_repl_example.py
+#
+# 3. mpremote will exit with an error after the previous step, because when the
+#    example runs the existing USB device disconnects and then re-enumerates with
+#    the second serial port. If you check (for example by running mpremote connect
+#    list) then you should now see two USB serial devices.
+#
+# 4. Connect to one of the new ports: mpremote connect PORTNAME
+#
+#    It may be necessary to type Ctrl-B to exit the raw REPL mode and resume the
+#    interactive REPL after mpremote connects.
+#
+# MIT license; Copyright (c) 2023-2024 Angus Gratton
+import os
+import time
+import usb.device
+from usb.device.cdc import CDCInterface
+
+cdc = CDCInterface()
+cdc.init(timeout=0)  # zero timeout makes this non-blocking, suitable for os.dupterm()
+
+# pass builtin_driver=True so that we get the built-in USB-CDC alongside,
+# if it's available.
+usb.device.get().init(cdc, builtin_driver=True)
+
+print("Waiting for USB host to configure the interface...")
+
+# wait for host enumerate as a CDC device...
+while not cdc.is_open():
+    time.sleep_ms(100)
+
+# Note: This example doesn't wait for the host to access the new CDC port,
+# which could be done by polling cdc.dtr, as this will block the REPL
+# from resuming while this code is still executing.
+
+print("CDC port enumerated, duplicating REPL...")
+
+old_term = os.dupterm(cdc)
diff --git a/micropython/usb/examples/device/hid_custom_keypad_example.py b/micropython/usb/examples/device/hid_custom_keypad_example.py
new file mode 100644
index 000000000..9d427cf10
--- /dev/null
+++ b/micropython/usb/examples/device/hid_custom_keypad_example.py
@@ -0,0 +1,144 @@
+# MicroPython USB HID custom Keypad example
+#
+# This example demonstrates creating a custom HID device with its own
+# HID descriptor, in this case for a USB number keypad.
+#
+# For higher level examples that require less code to use, see mouse_example.py
+# and keyboard_example.py
+#
+# To run this example:
+#
+# 1. Make sure `usb-device-hid` is installed via: mpremote mip install usb-device-hid
+#
+# 2. Run the example via: mpremote run hid_custom_keypad_example.py
+#
+# 3. mpremote will exit with an error after the previous step, because when the
+#    example runs the existing USB device disconnects and then re-enumerates with
+#    the custom HID interface present. At this point, the example is running.
+#
+# 4. To see output from the example, re-connect: mpremote connect PORTNAME
+#
+# MIT license; Copyright (c) 2023 Dave Wickham, 2023-2024 Angus Gratton
+from micropython import const
+import time
+import usb.device
+from usb.device.hid import HIDInterface
+
+_INTERFACE_PROTOCOL_KEYBOARD = const(0x01)
+
+
+def keypad_example():
+    k = KeypadInterface()
+
+    usb.device.get().init(k, builtin_driver=True)
+
+    while not k.is_open():
+        time.sleep_ms(100)
+
+    while True:
+        time.sleep(2)
+        print("Press NumLock...")
+        k.send_key("<NumLock>")
+        time.sleep_ms(100)
+        k.send_key()
+        time.sleep(1)
+        # continue
+        print("Press ...")
+        for _ in range(3):
+            time.sleep(0.1)
+            k.send_key(".")
+            time.sleep(0.1)
+            k.send_key()
+        print("Starting again...")
+
+
+class KeypadInterface(HIDInterface):
+    # Very basic synchronous USB keypad HID interface
+
+    def __init__(self):
+        super().__init__(
+            _KEYPAD_REPORT_DESC,
+            set_report_buf=bytearray(1),
+            protocol=_INTERFACE_PROTOCOL_KEYBOARD,
+            interface_str="MicroPython Keypad",
+        )
+        self.numlock = False
+
+    def on_set_report(self, report_data, _report_id, _report_type):
+        report = report_data[0]
+        b = bool(report & 1)
+        if b != self.numlock:
+            print("Numlock: ", b)
+            self.numlock = b
+
+    def send_key(self, key=None):
+        if key is None:
+            self.send_report(b"\x00")
+        else:
+            self.send_report(_key_to_id(key).to_bytes(1, "big"))
+
+
+# See HID Usages and Descriptions 1.4, section 10 Keyboard/Keypad Page (0x07)
+#
+# This keypad example has a contiguous series of keys (KEYPAD_KEY_IDS) starting
+# from the NumLock/Clear keypad key (0x53), but you can send any Key IDs from
+# the table in the HID Usages specification.
+_KEYPAD_KEY_OFFS = const(0x53)
+
+_KEYPAD_KEY_IDS = [
+    "<NumLock>",
+    "/",
+    "*",
+    "-",
+    "+",
+    "<Enter>",
+    "1",
+    "2",
+    "3",
+    "4",
+    "5",
+    "6",
+    "7",
+    "8",
+    "9",
+    "0",
+    ".",
+]
+
+
+def _key_to_id(key):
+    # This is a little slower than making a dict for lookup, but uses
+    # less memory and O(n) can be fast enough when n is small.
+    return _KEYPAD_KEY_IDS.index(key) + _KEYPAD_KEY_OFFS
+
+
+# HID Report descriptor for a numeric keypad
+#
+# fmt: off
+_KEYPAD_REPORT_DESC = (
+    b'\x05\x01'  # Usage Page (Generic Desktop)
+        b'\x09\x07'  # Usage (Keypad)
+    b'\xA1\x01'  # Collection (Application)
+        b'\x05\x07'  # Usage Page (Keypad)
+            b'\x19\x00'  # Usage Minimum (0)
+            b'\x29\xFF'  # Usage Maximum (ff)
+            b'\x15\x00'  # Logical Minimum (0)
+            b'\x25\xFF'  # Logical Maximum (ff)
+            b'\x95\x01'  # Report Count (1),
+            b'\x75\x08'  # Report Size (8),
+            b'\x81\x00'  # Input (Data, Array, Absolute)
+        b'\x05\x08'  # Usage page (LEDs)
+            b'\x19\x01'  # Usage Minimum (1)
+            b'\x29\x01'  # Usage Maximum (1),
+            b'\x95\x01'  # Report Count (1),
+            b'\x75\x01'  # Report Size (1),
+            b'\x91\x02'  # Output (Data, Variable, Absolute)
+            b'\x95\x01'  # Report Count (1),
+            b'\x75\x07'  # Report Size (7),
+            b'\x91\x01'  # Output (Constant) - padding bits
+    b'\xC0'  # End Collection
+)
+# fmt: on
+
+
+keypad_example()
diff --git a/micropython/usb/examples/device/keyboard_example.py b/micropython/usb/examples/device/keyboard_example.py
new file mode 100644
index 000000000..d8994ff1b
--- /dev/null
+++ b/micropython/usb/examples/device/keyboard_example.py
@@ -0,0 +1,97 @@
+# MicroPython USB Keyboard example
+#
+# To run this example:
+#
+# 1. Check the KEYS assignment below, and connect buttons or switches to the
+#    assigned GPIOs. You can change the entries as needed, look up the reference
+#    for your board to see what pins are available. Note that the example uses
+#    "active low" logic, so pressing a switch or button should switch the
+#    connected pin to Ground (0V).
+#
+# 2. Make sure `usb-device-keyboard` is installed via: mpremote mip install usb-device-keyboard
+#
+# 3. Run the example via: mpremote run keyboard_example.py
+#
+# 4. mpremote will exit with an error after the previous step, because when the
+#    example runs the existing USB device disconnects and then re-enumerates with
+#   the keyboard interface present. At this point, the example is running.
+#
+# 5. The example doesn't print anything to the serial port, but to stop it first
+#    re-connect: mpremote connect PORTNAME
+#
+# 6. Type Ctrl-C to interrupt the running example and stop it. You may have to
+#    also type Ctrl-B to restore the interactive REPL.
+#
+# To implement a keyboard with different USB HID characteristics, copy the
+# usb-device-keyboard/usb/device/keyboard.py file into your own project and modify
+# KeyboardInterface.
+#
+# MIT license; Copyright (c) 2024 Angus Gratton
+import usb.device
+from usb.device.keyboard import KeyboardInterface, KeyCode, LEDCode
+from machine import Pin
+import time
+
+# Tuples mapping Pin inputs to the KeyCode each input generates
+#
+# (Big keyboards usually multiplex multiple keys per input with a scan matrix,
+# but this is a simple example.)
+KEYS = (
+    (Pin.cpu.GPIO10, KeyCode.CAPS_LOCK),
+    (Pin.cpu.GPIO11, KeyCode.LEFT_SHIFT),
+    (Pin.cpu.GPIO12, KeyCode.M),
+    (Pin.cpu.GPIO13, KeyCode.P),
+    # ... add more pin to KeyCode mappings here if needed
+)
+
+# Tuples mapping Pin outputs to the LEDCode that turns the output on
+LEDS = (
+    (Pin.board.LED, LEDCode.CAPS_LOCK),
+    # ... add more pin to LEDCode mappings here if needed
+)
+
+
+class ExampleKeyboard(KeyboardInterface):
+    def on_led_update(self, led_mask):
+        # print(hex(led_mask))
+        for pin, code in LEDS:
+            # Set the pin high if 'code' bit is set in led_mask
+            pin(code & led_mask)
+
+
+def keyboard_example():
+    # Initialise all the pins as active-low inputs with pullup resistors
+    for pin, _ in KEYS:
+        pin.init(Pin.IN, Pin.PULL_UP)
+
+    # Initialise all the LEDs as active-high outputs
+    for pin, _ in LEDS:
+        pin.init(Pin.OUT, value=0)
+
+    # Register the keyboard interface and re-enumerate
+    k = ExampleKeyboard()
+    usb.device.get().init(k, builtin_driver=True)
+
+    print("Entering keyboard loop...")
+
+    keys = []  # Keys held down, reuse the same list object
+    prev_keys = [None]  # Previous keys, starts with a dummy value so first
+    # iteration will always send
+    while True:
+        if k.is_open():
+            keys.clear()
+            for pin, code in KEYS:
+                if not pin():  # active-low
+                    keys.append(code)
+            if keys != prev_keys:
+                # print(keys)
+                k.send_keys(keys)
+                prev_keys.clear()
+                prev_keys.extend(keys)
+
+        # This simple example scans each input in an infinite loop, but a more
+        # complex implementation would probably use a timer or similar.
+        time.sleep_ms(1)
+
+
+keyboard_example()
diff --git a/micropython/usb/examples/device/midi_example.py b/micropython/usb/examples/device/midi_example.py
new file mode 100644
index 000000000..55fe8af69
--- /dev/null
+++ b/micropython/usb/examples/device/midi_example.py
@@ -0,0 +1,78 @@
+# MicroPython USB MIDI example
+#
+# This example demonstrates creating a custom MIDI device.
+#
+# To run this example:
+#
+# 1. Make sure `usb-device-midi` is installed via: mpremote mip install usb-device-midi
+#
+# 2. Run the example via: mpremote run midi_example.py
+#
+# 3. mpremote will exit with an error after the previous step, because when the
+#    example runs the existing USB device disconnects and then re-enumerates with
+#    the MIDI interface present. At this point, the example is running.
+#
+# 4. To see output from the example, re-connect: mpremote connect PORTNAME
+#
+#
+# MIT license; Copyright (c) 2023-2024 Angus Gratton
+import usb.device
+from usb.device.midi import MIDIInterface
+import time
+
+
+class MIDIExample(MIDIInterface):
+    # Very simple example event handler functions, showing how to receive note
+    # and control change messages sent from the host to the device.
+    #
+    # If you need to send MIDI data to the host, then it's fine to instantiate
+    # MIDIInterface class directly.
+
+    def on_open(self):
+        super().on_open()
+        print("Device opened by host")
+
+    def on_note_on(self, channel, pitch, vel):
+        print(f"RX Note On channel {channel} pitch {pitch} velocity {vel}")
+
+    def on_note_off(self, channel, pitch, vel):
+        print(f"RX Note Off channel {channel} pitch {pitch} velocity {vel}")
+
+    def on_control_change(self, channel, controller, value):
+        print(f"RX Control channel {channel} controller {controller} value {value}")
+
+
+m = MIDIExample()
+# Remove builtin_driver=True if you don't want the MicroPython serial REPL available.
+usb.device.get().init(m, builtin_driver=True)
+
+print("Waiting for USB host to configure the interface...")
+
+while not m.is_open():
+    time.sleep_ms(100)
+
+print("Starting MIDI loop...")
+
+# TX constants
+CHANNEL = 0
+PITCH = 60
+CONTROLLER = 64
+
+control_val = 0
+
+while m.is_open():
+    time.sleep(1)
+    print(f"TX Note On channel {CHANNEL} pitch {PITCH}")
+    m.note_on(CHANNEL, PITCH)  # Velocity is an optional third argument
+    time.sleep(0.5)
+    print(f"TX Note Off channel {CHANNEL} pitch {PITCH}")
+    m.note_off(CHANNEL, PITCH)
+    time.sleep(1)
+    print(f"TX Control channel {CHANNEL} controller {CONTROLLER} value {control_val}")
+    m.control_change(CHANNEL, CONTROLLER, control_val)
+    control_val += 1
+    if control_val == 0x7F:
+        control_val = 0
+    time.sleep(1)
+
+print("USB host has reset device, example done.")
diff --git a/micropython/usb/examples/device/mouse_example.py b/micropython/usb/examples/device/mouse_example.py
new file mode 100644
index 000000000..c73d6cfa6
--- /dev/null
+++ b/micropython/usb/examples/device/mouse_example.py
@@ -0,0 +1,52 @@
+# MicroPython USB Mouse example
+#
+# To run this example:
+#
+# 1. Make sure `usb-device-mouse` is installed via: mpremote mip install usb-device-mouse
+#
+# 2. Run the example via: mpremote run mouse_example.py
+#
+# 3. mpremote will exit with an error after the previous step, because when the
+#    example runs the existing USB device disconnects and then re-enumerates with
+#    the mouse interface present. At this point, the example is running.
+#
+# 4. You should see the mouse move and right click. At this point, the example
+#    is finished executing.
+#
+# To implement a more complex mouse with more buttons or other custom interface
+# features, copy the usb-device-mouse/usb/device/mouse.py file into your own
+# project and modify MouseInterface.
+#
+# MIT license; Copyright (c) 2023-2024 Angus Gratton
+import time
+import usb.device
+from usb.device.mouse import MouseInterface
+
+
+def mouse_example():
+    m = MouseInterface()
+
+    # Note: builtin_driver=True means that if there's a USB-CDC REPL
+    # available then it will appear as well as the HID device.
+    usb.device.get().init(m, builtin_driver=True)
+
+    # wait for host to enumerate as a HID device...
+    while not m.is_open():
+        time.sleep_ms(100)
+
+    time.sleep_ms(2000)
+
+    print("Moving...")
+    m.move_by(-100, 0)
+    m.move_by(-100, 0)
+    time.sleep_ms(500)
+
+    print("Clicking...")
+    m.click_right(True)
+    time.sleep_ms(200)
+    m.click_right(False)
+
+    print("Done!")
+
+
+mouse_example()
diff --git a/micropython/usb/usb-device-cdc/manifest.py b/micropython/usb/usb-device-cdc/manifest.py
new file mode 100644
index 000000000..e844b6f01
--- /dev/null
+++ b/micropython/usb/usb-device-cdc/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.2")
+require("usb-device")
+package("usb")
diff --git a/micropython/usb/usb-device-cdc/usb/device/cdc.py b/micropython/usb/usb-device-cdc/usb/device/cdc.py
new file mode 100644
index 000000000..0acea184f
--- /dev/null
+++ b/micropython/usb/usb-device-cdc/usb/device/cdc.py
@@ -0,0 +1,444 @@
+# MicroPython USB CDC module
+# MIT license; Copyright (c) 2022 Martin Fischer, 2023-2024 Angus Gratton
+import io
+import time
+import errno
+import machine
+import struct
+from micropython import const
+
+from .core import Interface, Buffer, split_bmRequestType
+
+_EP_IN_FLAG = const(1 << 7)
+
+# Control transfer stages
+_STAGE_IDLE = const(0)
+_STAGE_SETUP = const(1)
+_STAGE_DATA = const(2)
+_STAGE_ACK = const(3)
+
+# Request types
+_REQ_TYPE_STANDARD = const(0x0)
+_REQ_TYPE_CLASS = const(0x1)
+_REQ_TYPE_VENDOR = const(0x2)
+_REQ_TYPE_RESERVED = const(0x3)
+
+_DEV_CLASS_MISC = const(0xEF)
+_CS_DESC_TYPE = const(0x24)  # CS Interface type communication descriptor
+
+# CDC control interface definitions
+_INTERFACE_CLASS_CDC = const(2)
+_INTERFACE_SUBCLASS_CDC = const(2)  # Abstract Control Mode
+_PROTOCOL_NONE = const(0)  # no protocol
+
+# CDC descriptor subtype
+# see also CDC120.pdf, table 13
+_CDC_FUNC_DESC_HEADER = const(0)
+_CDC_FUNC_DESC_CALL_MANAGEMENT = const(1)
+_CDC_FUNC_DESC_ABSTRACT_CONTROL = const(2)
+_CDC_FUNC_DESC_UNION = const(6)
+
+# CDC class requests, table 13, PSTN subclass
+_SET_LINE_CODING_REQ = const(0x20)
+_GET_LINE_CODING_REQ = const(0x21)
+_SET_CONTROL_LINE_STATE = const(0x22)
+_SEND_BREAK_REQ = const(0x23)
+
+_LINE_CODING_STOP_BIT_1 = const(0)
+_LINE_CODING_STOP_BIT_1_5 = const(1)
+_LINE_CODING_STOP_BIT_2 = const(2)
+
+_LINE_CODING_PARITY_NONE = const(0)
+_LINE_CODING_PARITY_ODD = const(1)
+_LINE_CODING_PARITY_EVEN = const(2)
+_LINE_CODING_PARITY_MARK = const(3)
+_LINE_CODING_PARITY_SPACE = const(4)
+
+_LINE_STATE_DTR = const(1)
+_LINE_STATE_RTS = const(2)
+
+_PARITY_BITS_REPR = "NOEMS"
+_STOP_BITS_REPR = ("1", "1.5", "2")
+
+# Other definitions
+_CDC_VERSION = const(0x0120)  # release number in binary-coded decimal
+
+# Number of endpoints in each interface
+_CDC_CONTROL_EP_NUM = const(1)
+_CDC_DATA_EP_NUM = const(2)
+
+# CDC data interface definitions
+_CDC_ITF_DATA_CLASS = const(0xA)
+_CDC_ITF_DATA_SUBCLASS = const(0)
+_CDC_ITF_DATA_PROT = const(0)  # no protocol
+
+# Length of the bulk transfer endpoints. Maybe should be configurable?
+_BULK_EP_LEN = const(64)
+
+# MicroPython error constants (negated as IOBase.ioctl uses negative return values for error codes)
+# these must match values in py/mperrno.h
+_MP_EINVAL = const(-22)
+_MP_ETIMEDOUT = const(-110)
+
+# MicroPython stream ioctl requests, same as py/stream.h
+_MP_STREAM_FLUSH = const(1)
+_MP_STREAM_POLL = const(3)
+
+# MicroPython ioctl poll values, same as py/stream.h
+_MP_STREAM_POLL_WR = const(0x04)
+_MP_STREAM_POLL_RD = const(0x01)
+_MP_STREAM_POLL_HUP = const(0x10)
+
+
+class CDCInterface(io.IOBase, Interface):
+    # USB CDC serial device class, designed to resemble machine.UART
+    # with some additional methods.
+    #
+    # Relies on multiple inheritance so it can be an io.IOBase for stream
+    # functions and also a Interface (actually an Interface Association
+    # Descriptor holding two interfaces.)
+    def __init__(self, **kwargs):
+        # io.IOBase has no __init__()
+        Interface.__init__(self)
+
+        # Callbacks for particular control changes initiated by the host
+        self.break_cb = None  # Host sent a "break" condition
+        self.line_state_cb = None
+        self.line_coding_cb = None
+
+        self._line_state = 0  # DTR & RTS
+        # Set a default line coding of 115200/8N1
+        self._line_coding = bytearray(b"\x00\xc2\x01\x00\x00\x00\x08")
+
+        self._wb = ()  # Optional write Buffer (IN endpoint), set by CDC.init()
+        self._rb = ()  # Optional read Buffer (OUT endpoint), set by CDC.init()
+        self._timeout = 1000  # set from CDC.init() as well
+
+        # one control interface endpoint, two data interface endpoints
+        self.ep_c_in = self.ep_d_in = self.ep_d_out = None
+
+        self._c_itf = None  # Number of control interface, data interface is one more
+
+        self.init(**kwargs)
+
+    def init(
+        self, baudrate=9600, bits=8, parity="N", stop=1, timeout=None, txbuf=256, rxbuf=256, flow=0
+    ):
+        # Configure the CDC serial port. Note that many of these settings like
+        # baudrate, bits, parity, stop don't change the USB-CDC device behavior
+        # at all, only the "line coding" as communicated from/to the USB host.
+
+        # Store initial line coding parameters in the USB CDC binary format
+        # (there is nothing implemented to further change these from Python
+        # code, the USB host sets them.)
+        struct.pack_into(
+            "<LBBB",
+            self._line_coding,
+            0,
+            baudrate,
+            _STOP_BITS_REPR.index(str(stop)),
+            _PARITY_BITS_REPR.index(parity),
+            bits,
+        )
+
+        if flow != 0:
+            raise NotImplementedError  # UART flow control currently not supported
+
+        if not (txbuf and rxbuf >= _BULK_EP_LEN):
+            raise ValueError  # Buffer sizes are required, rxbuf must be at least one EP
+
+        self._timeout = timeout
+        self._wb = Buffer(txbuf)
+        self._rb = Buffer(rxbuf)
+
+    ###
+    ### Line State & Line Coding State property getters
+    ###
+
+    @property
+    def rts(self):
+        return bool(self._line_state & _LINE_STATE_RTS)
+
+    @property
+    def dtr(self):
+        return bool(self._line_state & _LINE_STATE_DTR)
+
+    # Line Coding Representation
+    # Byte 0-3   Byte 4      Byte 5       Byte 6
+    # dwDTERate  bCharFormat bParityType  bDataBits
+
+    @property
+    def baudrate(self):
+        return struct.unpack("<LBBB", self._line_coding)[0]
+
+    @property
+    def stop_bits(self):
+        return _STOP_BITS_REPR[self._line_coding[4]]
+
+    @property
+    def parity(self):
+        return _PARITY_BITS_REPR[self._line_coding[5]]
+
+    @property
+    def data_bits(self):
+        return self._line_coding[6]
+
+    def __repr__(self):
+        return f"{self.baudrate}/{self.data_bits}{self.parity}{self.stop_bits} rts={self.rts} dtr={self.dtr}"
+
+    ###
+    ### Set callbacks for operations initiated by the host
+    ###
+
+    def set_break_cb(self, cb):
+        self.break_cb = cb
+
+    def set_line_state_cb(self, cb):
+        self.line_state_cb = cb
+
+    def set_line_coding_cb(self, cb):
+        self.line_coding_cb = cb
+
+    ###
+    ### USB Interface Implementation
+    ###
+
+    def desc_cfg(self, desc, itf_num, ep_num, strs):
+        # CDC needs a Interface Association Descriptor (IAD) wrapping two interfaces: Control & Data interfaces
+        desc.interface_assoc(itf_num, 2, _INTERFACE_CLASS_CDC, _INTERFACE_SUBCLASS_CDC)
+
+        # Now add the Control interface descriptor
+        self._c_itf = itf_num
+        desc.interface(itf_num, _CDC_CONTROL_EP_NUM, _INTERFACE_CLASS_CDC, _INTERFACE_SUBCLASS_CDC)
+
+        # Append the CDC class-specific interface descriptor
+        # see CDC120-track, p20
+        desc.pack(
+            "<BBBH",
+            5,  # bFunctionLength
+            _CS_DESC_TYPE,  # bDescriptorType
+            _CDC_FUNC_DESC_HEADER,  # bDescriptorSubtype
+            _CDC_VERSION,  # cdc version
+        )
+
+        # CDC-PSTN table3 "Call Management"
+        # set to No
+        desc.pack(
+            "<BBBBB",
+            5,  # bFunctionLength
+            _CS_DESC_TYPE,  # bDescriptorType
+            _CDC_FUNC_DESC_CALL_MANAGEMENT,  # bDescriptorSubtype
+            0,  # bmCapabilities - XXX no call managment so far
+            itf_num + 1,  # bDataInterface - interface 1
+        )
+
+        # CDC-PSTN table4 "Abstract Control"
+        # set to support line_coding and send_break
+        desc.pack(
+            "<BBBB",
+            4,  # bFunctionLength
+            _CS_DESC_TYPE,  # bDescriptorType
+            _CDC_FUNC_DESC_ABSTRACT_CONTROL,  # bDescriptorSubtype
+            0x6,  # bmCapabilities D1, D2
+        )
+
+        # CDC-PSTN "Union"
+        # set control interface / data interface number
+        desc.pack(
+            "<BBBBB",
+            5,  # bFunctionLength
+            _CS_DESC_TYPE,  # bDescriptorType
+            _CDC_FUNC_DESC_UNION,  # bDescriptorSubtype
+            itf_num,  # bControlInterface
+            itf_num + 1,  # bSubordinateInterface0 (data class itf number)
+        )
+
+        # Single control IN endpoint (currently unused in this implementation)
+        self.ep_c_in = ep_num | _EP_IN_FLAG
+        desc.endpoint(self.ep_c_in, "interrupt", 8, 16)
+
+        # Now add the data interface
+        desc.interface(
+            itf_num + 1,
+            _CDC_DATA_EP_NUM,
+            _CDC_ITF_DATA_CLASS,
+            _CDC_ITF_DATA_SUBCLASS,
+            _CDC_ITF_DATA_PROT,
+        )
+
+        # Two data endpoints, bulk OUT and IN
+        self.ep_d_out = ep_num + 1
+        self.ep_d_in = (ep_num + 1) | _EP_IN_FLAG
+        desc.endpoint(self.ep_d_out, "bulk", _BULK_EP_LEN, 0)
+        desc.endpoint(self.ep_d_in, "bulk", _BULK_EP_LEN, 0)
+
+    def num_itfs(self):
+        return 2
+
+    def num_eps(self):
+        return 2  # total after masking out _EP_IN_FLAG
+
+    def on_open(self):
+        super().on_open()
+        # kick off any transfers that may have queued while the device was not open
+        self._rd_xfer()
+        self._wr_xfer()
+
+    def on_interface_control_xfer(self, stage, request):
+        # Handle class-specific interface control transfers
+        bmRequestType, bRequest, wValue, wIndex, wLength = struct.unpack("BBHHH", request)
+        recipient, req_type, req_dir = split_bmRequestType(bmRequestType)
+
+        if wIndex != self._c_itf:
+            return False  # Only for the control interface (may be redundant check?)
+
+        if req_type != _REQ_TYPE_CLASS:
+            return False  # Unsupported request type
+
+        if stage == _STAGE_SETUP:
+            if bRequest in (_SET_LINE_CODING_REQ, _GET_LINE_CODING_REQ):
+                return self._line_coding  # Buffer to read or write
+
+            # Continue on other supported requests, stall otherwise
+            return bRequest in (_SET_CONTROL_LINE_STATE, _SEND_BREAK_REQ)
+
+        if stage == _STAGE_ACK:
+            if bRequest == _SET_LINE_CODING_REQ:
+                if self.line_coding_cb:
+                    self.line_coding_cb(self._line_coding)
+            elif bRequest == _SET_CONTROL_LINE_STATE:
+                self._line_state = wValue
+                if self.line_state_cb:
+                    self.line_state_cb(wValue)
+            elif bRequest == _SEND_BREAK_REQ:
+                if self.break_cb:
+                    self.break_cb(wValue)
+
+        return True  # allow DATA/ACK stages to complete normally
+
+    def _wr_xfer(self):
+        # Submit a new data IN transfer from the _wb buffer, if needed
+        if self.is_open() and not self.xfer_pending(self.ep_d_in) and self._wb.readable():
+            self.submit_xfer(self.ep_d_in, self._wb.pend_read(), self._wr_cb)
+
+    def _wr_cb(self, ep, res, num_bytes):
+        # Whenever a data IN transfer ends
+        if res == 0:
+            self._wb.finish_read(num_bytes)
+        self._wr_xfer()
+
+    def _rd_xfer(self):
+        # Keep an active data OUT transfer to read data from the host,
+        # whenever the receive buffer has room for new data
+        if (
+            self.is_open()
+            and not self.xfer_pending(self.ep_d_out)
+            and self._rb.writable() >= _BULK_EP_LEN
+        ):
+            # Can only submit up to the endpoint length per transaction, otherwise we won't
+            # get any transfer callback until the full transaction completes.
+            self.submit_xfer(self.ep_d_out, self._rb.pend_write(_BULK_EP_LEN), self._rd_cb)
+
+    def _rd_cb(self, ep, res, num_bytes):
+        # Whenever a data OUT transfer ends
+        if res == 0:
+            self._rb.finish_write(num_bytes)
+        self._rd_xfer()
+
+    ###
+    ### io.IOBase stream implementation
+    ###
+
+    def write(self, buf):
+        start = time.ticks_ms()
+        mv = buf
+
+        while True:
+            # Keep pushing buf into _wb into it's all gone
+            nbytes = self._wb.write(mv)
+            self._wr_xfer()  # make sure a transfer is running from _wb
+
+            if nbytes == len(mv):
+                return len(buf)  # Success
+
+            # if buf couldn't be fully written on the first attempt
+            # convert it to a memoryview to track partial writes
+            if mv is buf:
+                mv = memoryview(buf)
+            mv = mv[nbytes:]
+
+            # check for timeout
+            if time.ticks_diff(time.ticks_ms(), start) >= self._timeout:
+                return len(buf) - len(mv)
+
+            machine.idle()
+
+    def read(self, size):
+        start = time.ticks_ms()
+
+        # Allocate a suitable buffer to read into
+        if size >= 0:
+            b = bytearray(size)
+        else:
+            # for size == -1, return however many bytes are ready
+            b = bytearray(self._rb.readable())
+
+        n = self._readinto(b, start)
+        if not n:
+            return None
+        if n < len(b):
+            return b[:n]
+        return b
+
+    def readinto(self, b):
+        return self._readinto(b, time.ticks_ms())
+
+    def _readinto(self, b, start):
+        if len(b) == 0:
+            return 0
+
+        n = 0
+        m = memoryview(b)
+        while n < len(b):
+            # copy out of the read buffer if there is anything available
+            if self._rb.readable():
+                n += self._rb.readinto(m if n == 0 else m[n:])
+                self._rd_xfer()  # if _rd was previously full, no transfer will be running
+                if n == len(b):
+                    break  # Done, exit before we call machine.idle()
+
+            if time.ticks_diff(time.ticks_ms(), start) >= self._timeout:
+                break  # Timed out
+
+            machine.idle()
+
+        return n or None
+
+    def ioctl(self, req, arg):
+        if req == _MP_STREAM_POLL:
+            return (
+                (_MP_STREAM_POLL_WR if (arg & _MP_STREAM_POLL_WR) and self._wb.writable() else 0)
+                | (_MP_STREAM_POLL_RD if (arg & _MP_STREAM_POLL_RD) and self._rb.readable() else 0)
+                |
+                # using the USB level "open" (i.e. connected to host) for !HUP, not !DTR (port is open)
+                (_MP_STREAM_POLL_HUP if (arg & _MP_STREAM_POLL_HUP) and not self.is_open() else 0)
+            )
+        elif req == _MP_STREAM_FLUSH:
+            start = time.ticks_ms()
+            # Wait until write buffer contains no bytes for the lower TinyUSB layer to "read"
+            while self._wb.readable():
+                if not self.is_open():
+                    return _MP_EINVAL
+                if time.ticks_diff(time.ticks_ms(), start) > self._timeout:
+                    return _MP_ETIMEDOUT
+                machine.idle()
+            return 0
+
+        return _MP_EINVAL
+
+    def flush(self):
+        # a C implementation of this exists in stream.c, but it's not in io.IOBase
+        # and can't immediately be called from here (AFAIK)
+        r = self.ioctl(_MP_STREAM_FLUSH, 0)
+        if r:
+            raise OSError(r)
diff --git a/micropython/usb/usb-device-hid/manifest.py b/micropython/usb/usb-device-hid/manifest.py
new file mode 100644
index 000000000..4520325e3
--- /dev/null
+++ b/micropython/usb/usb-device-hid/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.1")
+require("usb-device")
+package("usb")
diff --git a/micropython/usb/usb-device-hid/usb/device/hid.py b/micropython/usb/usb-device-hid/usb/device/hid.py
new file mode 100644
index 000000000..1c1c9ac6b
--- /dev/null
+++ b/micropython/usb/usb-device-hid/usb/device/hid.py
@@ -0,0 +1,233 @@
+# MicroPython USB hid module
+#
+# This implements a base HIDInterface class that can be used directly,
+# or subclassed into more specific HID interface types.
+#
+# MIT license; Copyright (c) 2023 Angus Gratton
+from micropython import const
+import machine
+import struct
+import time
+from .core import Interface, Descriptor, split_bmRequestType
+
+_EP_IN_FLAG = const(1 << 7)
+
+# Control transfer stages
+_STAGE_IDLE = const(0)
+_STAGE_SETUP = const(1)
+_STAGE_DATA = const(2)
+_STAGE_ACK = const(3)
+
+# Request types
+_REQ_TYPE_STANDARD = const(0x0)
+_REQ_TYPE_CLASS = const(0x1)
+_REQ_TYPE_VENDOR = const(0x2)
+_REQ_TYPE_RESERVED = const(0x3)
+
+# Descriptor types
+_DESC_HID_TYPE = const(0x21)
+_DESC_REPORT_TYPE = const(0x22)
+_DESC_PHYSICAL_TYPE = const(0x23)
+
+# Interface and protocol identifiers
+_INTERFACE_CLASS = const(0x03)
+_INTERFACE_SUBCLASS_NONE = const(0x00)
+_INTERFACE_SUBCLASS_BOOT = const(0x01)
+
+_INTERFACE_PROTOCOL_NONE = const(0x00)
+_INTERFACE_PROTOCOL_KEYBOARD = const(0x01)
+_INTERFACE_PROTOCOL_MOUSE = const(0x02)
+
+# bRequest values for HID control requests
+_REQ_CONTROL_GET_REPORT = const(0x01)
+_REQ_CONTROL_GET_IDLE = const(0x02)
+_REQ_CONTROL_GET_PROTOCOL = const(0x03)
+_REQ_CONTROL_GET_DESCRIPTOR = const(0x06)
+_REQ_CONTROL_SET_REPORT = const(0x09)
+_REQ_CONTROL_SET_IDLE = const(0x0A)
+_REQ_CONTROL_SET_PROTOCOL = const(0x0B)
+
+# Standard descriptor lengths
+_STD_DESC_INTERFACE_LEN = const(9)
+_STD_DESC_ENDPOINT_LEN = const(7)
+
+
+class HIDInterface(Interface):
+    # Abstract base class to implement a USB device HID interface in Python.
+
+    def __init__(
+        self,
+        report_descriptor,
+        extra_descriptors=[],
+        set_report_buf=None,
+        protocol=_INTERFACE_PROTOCOL_NONE,
+        interface_str=None,
+    ):
+        # Construct a new HID interface.
+        #
+        # - report_descriptor is the only mandatory argument, which is the binary
+        # data consisting of the HID Report Descriptor. See Device Class
+        # Definition for Human Interface Devices (HID) v1.11 section 6.2.2 Report
+        # Descriptor, p23.
+        #
+        # - extra_descriptors is an optional argument holding additional HID
+        #   descriptors, to append after the mandatory report descriptor. Most
+        #   HID devices do not use these.
+        #
+        # - set_report_buf is an optional writable buffer object (i.e.
+        #   bytearray), where SET_REPORT requests from the host can be
+        #   written. Only necessary if the report_descriptor contains Output
+        #   entries. If set, the size must be at least the size of the largest
+        #   Output entry.
+        #
+        # - protocol can be set to a specific value as per HID v1.11 section 4.3 Protocols, p9.
+        #
+        # - interface_str is an optional string descriptor to associate with the HID USB interface.
+        super().__init__()
+        self.report_descriptor = report_descriptor
+        self.extra_descriptors = extra_descriptors
+        self._set_report_buf = set_report_buf
+        self.protocol = protocol
+        self.interface_str = interface_str
+
+        self._int_ep = None  # set during enumeration
+
+    def get_report(self):
+        return False
+
+    def on_set_report(self, report_data, report_id, report_type):
+        # Override this function in order to handle SET REPORT requests from the host,
+        # where it sends data to the HID device.
+        #
+        # This function will only be called if the Report descriptor contains at least one Output entry,
+        # and the set_report_buf argument is provided to the constructor.
+        #
+        # Return True to complete the control transfer normally, False to abort it.
+        return True
+
+    def busy(self):
+        # Returns True if the interrupt endpoint is busy (i.e. existing transfer is pending)
+        return self.is_open() and self.xfer_pending(self._int_ep)
+
+    def send_report(self, report_data, timeout_ms=100):
+        # Helper function to send a HID report in the typical USB interrupt
+        # endpoint associated with a HID interface.
+        #
+        # Returns True if successful, False if HID device is not active or timeout
+        # is reached without being able to queue the report for sending.
+        deadline = time.ticks_add(time.ticks_ms(), timeout_ms)
+        while self.busy():
+            if time.ticks_diff(deadline, time.ticks_ms()) <= 0:
+                return False
+            machine.idle()
+        if not self.is_open():
+            return False
+        self.submit_xfer(self._int_ep, report_data)
+        return True
+
+    def desc_cfg(self, desc, itf_num, ep_num, strs):
+        # Add the standard interface descriptor
+        desc.interface(
+            itf_num,
+            1,
+            _INTERFACE_CLASS,
+            _INTERFACE_SUBCLASS_NONE,
+            self.protocol,
+            len(strs) if self.interface_str else 0,
+        )
+
+        if self.interface_str:
+            strs.append(self.interface_str)
+
+        # As per HID v1.11 section 7.1 Standard Requests, return the contents of
+        # the standard HID descriptor before the associated endpoint descriptor.
+        self.get_hid_descriptor(desc)
+
+        # Add the typical single USB interrupt endpoint descriptor associated
+        # with a HID interface.
+        self._int_ep = ep_num | _EP_IN_FLAG
+        desc.endpoint(self._int_ep, "interrupt", 8, 8)
+
+        self.idle_rate = 0
+        self.protocol = 0
+
+    def num_eps(self):
+        return 1
+
+    def get_hid_descriptor(self, desc=None):
+        # Append a full USB HID descriptor from the object's report descriptor
+        # and optional additional descriptors.
+        #
+        # See HID Specification Version 1.1, Section 6.2.1 HID Descriptor p22
+
+        l = 9 + 3 * len(self.extra_descriptors)  # total length
+
+        if desc is None:
+            desc = Descriptor(bytearray(l))
+
+        desc.pack(
+            "<BBHBBBH",
+            l,  # bLength
+            _DESC_HID_TYPE,  # bDescriptorType
+            0x111,  # bcdHID
+            0,  # bCountryCode
+            len(self.extra_descriptors) + 1,  # bNumDescriptors
+            0x22,  # bDescriptorType, Report
+            len(self.report_descriptor),  # wDescriptorLength, Report
+        )
+        # Fill in any additional descriptor type/length pairs
+        #
+        # TODO: unclear if this functionality is ever used, may be easier to not
+        # support in base class
+        for dt, dd in self.extra_descriptors:
+            desc.pack("<BH", dt, len(dd))
+
+        return desc.b
+
+    def on_interface_control_xfer(self, stage, request):
+        # Handle standard and class-specific interface control transfers for HID devices.
+        bmRequestType, bRequest, wValue, _, wLength = struct.unpack("BBHHH", request)
+
+        recipient, req_type, _ = split_bmRequestType(bmRequestType)
+
+        if stage == _STAGE_SETUP:
+            if req_type == _REQ_TYPE_STANDARD:
+                # HID Spec p48: 7.1 Standard Requests
+                if bRequest == _REQ_CONTROL_GET_DESCRIPTOR:
+                    desc_type = wValue >> 8
+                    if desc_type == _DESC_HID_TYPE:
+                        return self.get_hid_descriptor()
+                    if desc_type == _DESC_REPORT_TYPE:
+                        return self.report_descriptor
+            elif req_type == _REQ_TYPE_CLASS:
+                # HID Spec p50: 7.2 Class-Specific Requests
+                if bRequest == _REQ_CONTROL_GET_REPORT:
+                    print("GET_REPORT?")
+                    return False  # Unsupported for now
+                if bRequest == _REQ_CONTROL_GET_IDLE:
+                    return bytes([self.idle_rate])
+                if bRequest == _REQ_CONTROL_GET_PROTOCOL:
+                    return bytes([self.protocol])
+                if bRequest in (_REQ_CONTROL_SET_IDLE, _REQ_CONTROL_SET_PROTOCOL):
+                    return True
+                if bRequest == _REQ_CONTROL_SET_REPORT:
+                    return self._set_report_buf  # If None, request will stall
+            return False  # Unsupported request
+
+        if stage == _STAGE_ACK:
+            if req_type == _REQ_TYPE_CLASS:
+                if bRequest == _REQ_CONTROL_SET_IDLE:
+                    self.idle_rate = wValue >> 8
+                elif bRequest == _REQ_CONTROL_SET_PROTOCOL:
+                    self.protocol = wValue
+                elif bRequest == _REQ_CONTROL_SET_REPORT:
+                    report_id = wValue & 0xFF
+                    report_type = wValue >> 8
+                    report_data = self._set_report_buf
+                    if wLength < len(report_data):
+                        # need to truncate the response in the callback if we got less bytes
+                        # than allowed for in the buffer
+                        report_data = memoryview(self._set_report_buf)[:wLength]
+                    self.on_set_report(report_data, report_id, report_type)
+
+        return True  # allow DATA/ACK stages to complete normally
diff --git a/micropython/usb/usb-device-keyboard/manifest.py b/micropython/usb/usb-device-keyboard/manifest.py
new file mode 100644
index 000000000..5a2ff307d
--- /dev/null
+++ b/micropython/usb/usb-device-keyboard/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.1")
+require("usb-device-hid")
+package("usb")
diff --git a/micropython/usb/usb-device-keyboard/usb/device/keyboard.py b/micropython/usb/usb-device-keyboard/usb/device/keyboard.py
new file mode 100644
index 000000000..22091c50b
--- /dev/null
+++ b/micropython/usb/usb-device-keyboard/usb/device/keyboard.py
@@ -0,0 +1,233 @@
+# MIT license; Copyright (c) 2023-2024 Angus Gratton
+from micropython import const
+import time
+import usb.device
+from usb.device.hid import HIDInterface
+
+_INTERFACE_PROTOCOL_KEYBOARD = const(0x01)
+
+_KEY_ARRAY_LEN = const(6)  # Size of HID key array, must match report descriptor
+_KEY_REPORT_LEN = const(_KEY_ARRAY_LEN + 2)  # Modifier Byte + Reserved Byte + Array entries
+
+
+class KeyboardInterface(HIDInterface):
+    # Synchronous USB keyboard HID interface
+
+    def __init__(self):
+        super().__init__(
+            _KEYBOARD_REPORT_DESC,
+            set_report_buf=bytearray(1),
+            protocol=_INTERFACE_PROTOCOL_KEYBOARD,
+            interface_str="MicroPython Keyboard",
+        )
+        self._key_reports = [
+            bytearray(_KEY_REPORT_LEN),
+            bytearray(_KEY_REPORT_LEN),
+        ]  # Ping/pong report buffers
+        self.numlock = False
+
+    def on_set_report(self, report_data, _report_id, _report_type):
+        self.on_led_update(report_data[0])
+
+    def on_led_update(self, led_mask):
+        # Override to handle keyboard LED updates. led_mask is bitwise ORed
+        # together values as defined in LEDCode.
+        pass
+
+    def send_keys(self, down_keys, timeout_ms=100):
+        # Update the state of the keyboard by sending a report with down_keys
+        # set, where down_keys is an iterable (list or similar) of integer
+        # values such as the values defined in KeyCode.
+        #
+        # Will block for up to timeout_ms if a previous report is still
+        # pending to be sent to the host. Returns True on success.
+
+        r, s = self._key_reports  # next report buffer to send, spare report buffer
+        r[0] = 0  # modifier byte
+        i = 2  # index for next key array item to write to
+        for k in down_keys:
+            if k < 0:  # Modifier key
+                r[0] |= -k
+            elif i < _KEY_REPORT_LEN:
+                r[i] = k
+                i += 1
+            else:  # Excess rollover! Can't report
+                r[0] = 0
+                for i in range(2, _KEY_REPORT_LEN):
+                    r[i] = 0xFF
+                break
+
+        while i < _KEY_REPORT_LEN:
+            r[i] = 0
+            i += 1
+
+        if self.send_report(r, timeout_ms):
+            # Swap buffers if the previous one is newly queued to send, so
+            # any subsequent call can't modify that buffer mid-send
+            self._key_reports[0] = s
+            self._key_reports[1] = r
+            return True
+        return False
+
+
+# HID keyboard report descriptor
+#
+# From p69 of http://www.usb.org/developers/devclass_docs/HID1_11.pdf
+#
+# fmt: off
+_KEYBOARD_REPORT_DESC = (
+    b'\x05\x01'     # Usage Page (Generic Desktop),
+        b'\x09\x06'     # Usage (Keyboard),
+    b'\xA1\x01'     # Collection (Application),
+        b'\x05\x07'         # Usage Page (Key Codes);
+            b'\x19\xE0'         # Usage Minimum (224),
+            b'\x29\xE7'         # Usage Maximum (231),
+            b'\x15\x00'         # Logical Minimum (0),
+            b'\x25\x01'         # Logical Maximum (1),
+            b'\x75\x01'         # Report Size (1),
+            b'\x95\x08'         # Report Count (8),
+            b'\x81\x02'         # Input (Data, Variable, Absolute), ;Modifier byte
+            b'\x95\x01'         # Report Count (1),
+            b'\x75\x08'         # Report Size (8),
+            b'\x81\x01'         # Input (Constant), ;Reserved byte
+            b'\x95\x05'         # Report Count (5),
+            b'\x75\x01'         # Report Size (1),
+        b'\x05\x08'         # Usage Page (Page# for LEDs),
+            b'\x19\x01'         # Usage Minimum (1),
+            b'\x29\x05'         # Usage Maximum (5),
+            b'\x91\x02'         # Output (Data, Variable, Absolute), ;LED report
+            b'\x95\x01'         # Report Count (1),
+            b'\x75\x03'         # Report Size (3),
+            b'\x91\x01'         # Output (Constant), ;LED report padding
+            b'\x95\x06'         # Report Count (6),
+            b'\x75\x08'         # Report Size (8),
+            b'\x15\x00'         # Logical Minimum (0),
+            b'\x25\x65'         # Logical Maximum(101),
+        b'\x05\x07'         # Usage Page (Key Codes),
+            b'\x19\x00'         # Usage Minimum (0),
+            b'\x29\x65'         # Usage Maximum (101),
+            b'\x81\x00'         # Input (Data, Array), ;Key arrays (6 bytes)
+    b'\xC0'     # End Collection
+)
+# fmt: on
+
+
+# Standard HID keycodes, as a pseudo-enum class for easy access
+#
+# Modifier keys are encoded as negative values
+class KeyCode:
+    A = 4
+    B = 5
+    C = 6
+    D = 7
+    E = 8
+    F = 9
+    G = 10
+    H = 11
+    I = 12
+    J = 13
+    K = 14
+    L = 15
+    M = 16
+    N = 17
+    O = 18
+    P = 19
+    Q = 20
+    R = 21
+    S = 22
+    T = 23
+    U = 24
+    V = 25
+    W = 26
+    X = 27
+    Y = 28
+    Z = 29
+    N1 = 30  # Standard number row keys
+    N2 = 31
+    N3 = 32
+    N4 = 33
+    N5 = 34
+    N6 = 35
+    N7 = 36
+    N8 = 37
+    N9 = 38
+    N0 = 39
+    ENTER = 40
+    ESCAPE = 41
+    BACKSPACE = 42
+    TAB = 43
+    SPACE = 44
+    MINUS = 45  # - _
+    EQUAL = 46  # = +
+    OPEN_BRACKET = 47  # [ {
+    CLOSE_BRACKET = 48  # ] }
+    BACKSLASH = 49  # \ |
+    HASH = 50  # # ~
+    SEMICOLON = 51  # ; :
+    QUOTE = 52  # ' "
+    GRAVE = 53  # ` ~
+    COMMA = 54  # , <
+    DOT = 55  # . >
+    SLASH = 56  # / ?
+    CAPS_LOCK = 57
+    F1 = 58
+    F2 = 59
+    F3 = 60
+    F4 = 61
+    F5 = 62
+    F6 = 63
+    F7 = 64
+    F8 = 65
+    F9 = 66
+    F10 = 67
+    F11 = 68
+    F12 = 69
+    PRINTSCREEN = 70
+    SCROLL_LOCK = 71
+    PAUSE = 72
+    INSERT = 73
+    HOME = 74
+    PAGEUP = 75
+    DELETE = 76
+    END = 77
+    PAGEDOWN = 78
+    RIGHT = 79  # Arrow keys
+    LEFT = 80
+    DOWN = 81
+    UP = 82
+    KP_NUM_LOCK = 83
+    KP_DIVIDE = 84
+    KP_AT = 85
+    KP_MULTIPLY = 85
+    KP_MINUS = 86
+    KP_PLUS = 87
+    KP_ENTER = 88
+    KP_1 = 89
+    KP_2 = 90
+    KP_3 = 91
+    KP_4 = 92
+    KP_5 = 93
+    KP_6 = 94
+    KP_7 = 95
+    KP_8 = 96
+    KP_9 = 97
+    KP_0 = 98
+
+    # HID modifier values (negated to allow them to be passed along with the normal keys)
+    LEFT_CTRL = -0x01
+    LEFT_SHIFT = -0x02
+    LEFT_ALT = -0x04
+    LEFT_UI = -0x08
+    RIGHT_CTRL = -0x10
+    RIGHT_SHIFT = -0x20
+    RIGHT_ALT = -0x40
+    RIGHT_UI = -0x80
+
+
+# HID LED values
+class LEDCode:
+    NUM_LOCK = 0x01
+    CAPS_LOCK = 0x02
+    SCROLL_LOCK = 0x04
+    COMPOSE = 0x08
+    KANA = 0x10
diff --git a/micropython/usb/usb-device-midi/manifest.py b/micropython/usb/usb-device-midi/manifest.py
new file mode 100644
index 000000000..af9b8cb84
--- /dev/null
+++ b/micropython/usb/usb-device-midi/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.0")
+require("usb-device")
+package("usb")
diff --git a/micropython/usb/usb-device-midi/usb/device/midi.py b/micropython/usb/usb-device-midi/usb/device/midi.py
new file mode 100644
index 000000000..ecb178ea4
--- /dev/null
+++ b/micropython/usb/usb-device-midi/usb/device/midi.py
@@ -0,0 +1,306 @@
+# MicroPython USB MIDI module
+# MIT license; Copyright (c) 2023 Paul Hamshere, 2023-2024 Angus Gratton
+from micropython import const, schedule
+import struct
+
+from .core import Interface, Buffer
+
+_EP_IN_FLAG = const(1 << 7)
+
+_INTERFACE_CLASS_AUDIO = const(0x01)
+_INTERFACE_SUBCLASS_AUDIO_CONTROL = const(0x01)
+_INTERFACE_SUBCLASS_AUDIO_MIDISTREAMING = const(0x03)
+
+# Audio subclass extends the standard endpoint descriptor
+# with two extra bytes
+_STD_DESC_AUDIO_ENDPOINT_LEN = const(9)
+_CLASS_DESC_ENDPOINT_LEN = const(5)
+
+_STD_DESC_ENDPOINT_TYPE = const(0x5)
+
+_JACK_TYPE_EMBEDDED = const(0x01)
+_JACK_TYPE_EXTERNAL = const(0x02)
+
+_JACK_IN_DESC_LEN = const(6)
+_JACK_OUT_DESC_LEN = const(9)
+
+# MIDI Status bytes. For Channel messages these are only the upper 4 bits, ORed with the channel number.
+# As per https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message
+_MIDI_NOTE_OFF = const(0x80)
+_MIDI_NOTE_ON = const(0x90)
+_MIDI_POLY_KEYPRESS = const(0xA0)
+_MIDI_CONTROL_CHANGE = const(0xB0)
+
+# USB-MIDI CINs (Code Index Numbers), as per USB MIDI Table 4-1
+_CIN_SYS_COMMON_2BYTE = const(0x2)
+_CIN_SYS_COMMON_3BYTE = const(0x3)
+_CIN_SYSEX_START = const(0x4)
+_CIN_SYSEX_END_1BYTE = const(0x5)
+_CIN_SYSEX_END_2BYTE = const(0x6)
+_CIN_SYSEX_END_3BYTE = const(0x7)
+_CIN_NOTE_OFF = const(0x8)
+_CIN_NOTE_ON = const(0x9)
+_CIN_POLY_KEYPRESS = const(0xA)
+_CIN_CONTROL_CHANGE = const(0xB)
+_CIN_PROGRAM_CHANGE = const(0xC)
+_CIN_CHANNEL_PRESSURE = const(0xD)
+_CIN_PITCH_BEND = const(0xE)
+_CIN_SINGLE_BYTE = const(0xF)  # Not currently supported
+
+# Jack IDs for a simple bidrectional MIDI device(!)
+_EMB_IN_JACK_ID = const(1)
+_EXT_IN_JACK_ID = const(2)
+_EMB_OUT_JACK_ID = const(3)
+_EXT_OUT_JACK_ID = const(4)
+
+# Data flows, as modelled by USB-MIDI and this hypothetical interface, are as follows:
+# Device RX = USB OUT EP => _EMB_IN_JACK => _EMB_OUT_JACK
+# Device TX = _EXT_IN_JACK => _EMB_OUT_JACK => USB IN EP
+
+
+class MIDIInterface(Interface):
+    # Base class to implement a USB MIDI device in Python.
+    #
+    # To be compliant this also regisers a dummy USB Audio interface, but that
+    # interface isn't otherwise used.
+
+    def __init__(self, rxlen=16, txlen=16):
+        # Arguments are size of transmit and receive buffers in bytes.
+
+        super().__init__()
+        self.ep_out = None  # Set during enumeration. RX direction (host to device)
+        self.ep_in = None  # TX direction (device to host)
+        self._rx = Buffer(rxlen)
+        self._tx = Buffer(txlen)
+
+    # Callbacks for handling received MIDI messages.
+    #
+    # Subclasses can choose between overriding on_midi_event
+    # and handling all MIDI events manually, or overriding the
+    # functions for note on/off and control change, only.
+
+    def on_midi_event(self, cin, midi0, midi1, midi2):
+        ch = midi0 & 0x0F
+        if cin == _CIN_NOTE_ON:
+            self.on_note_on(ch, midi1, midi2)
+        elif cin == _CIN_NOTE_OFF:
+            self.on_note_off(ch, midi1, midi2)
+        elif cin == _CIN_CONTROL_CHANGE:
+            self.on_control_change(ch, midi1, midi2)
+
+    def on_note_on(self, channel, pitch, vel):
+        pass  # Override to handle Note On messages
+
+    def on_note_off(self, channel, pitch, vel):
+        pass  # Override to handle Note On messages
+
+    def on_control_change(self, channel, controller, value):
+        pass  # Override to handle Control Change messages
+
+    # Helper functions for sending common MIDI messages
+
+    def note_on(self, channel, pitch, vel=0x40):
+        self.send_event(_CIN_NOTE_ON, _MIDI_NOTE_ON | channel, pitch, vel)
+
+    def note_off(self, channel, pitch, vel=0x40):
+        self.send_event(_CIN_NOTE_OFF, _MIDI_NOTE_OFF | channel, pitch, vel)
+
+    def control_change(self, channel, controller, value):
+        self.send_event(_CIN_CONTROL_CHANGE, _MIDI_CONTROL_CHANGE | channel, controller, value)
+
+    def send_event(self, cin, midi0, midi1=0, midi2=0):
+        # Queue a MIDI Event Packet to send to the host.
+        #
+        # CIN = USB-MIDI Code Index Number, see USB MIDI 1.0 section 4 "USB-MIDI Event Packets"
+        #
+        # Remaining arguments are 0-3 MIDI data bytes.
+        #
+        # Note this function returns when the MIDI Event Packet has been queued,
+        # not when it's been received by the host.
+        #
+        # Returns False if the TX buffer is full and the MIDI Event could not be queued.
+        w = self._tx.pend_write()
+        if len(w) < 4:
+            return False  # TX buffer is full. TODO: block here?
+        w[0] = cin  # leave cable number as 0?
+        w[1] = midi0
+        w[2] = midi1
+        w[3] = midi2
+        self._tx.finish_write(4)
+        self._tx_xfer()
+        return True
+
+    def _tx_xfer(self):
+        # Keep an active IN transfer to send data to the host, whenever
+        # there is data to send.
+        if self.is_open() and not self.xfer_pending(self.ep_in) and self._tx.readable():
+            self.submit_xfer(self.ep_in, self._tx.pend_read(), self._tx_cb)
+
+    def _tx_cb(self, ep, res, num_bytes):
+        if res == 0:
+            self._tx.finish_read(num_bytes)
+        self._tx_xfer()
+
+    def _rx_xfer(self):
+        # Keep an active OUT transfer to receive MIDI events from the host
+        if self.is_open() and not self.xfer_pending(self.ep_out) and self._rx.writable():
+            self.submit_xfer(self.ep_out, self._rx.pend_write(), self._rx_cb)
+
+    def _rx_cb(self, ep, res, num_bytes):
+        if res == 0:
+            self._rx.finish_write(num_bytes)
+            schedule(self._on_rx, None)
+        self._rx_xfer()
+
+    def on_open(self):
+        super().on_open()
+        # kick off any transfers that may have queued while the device was not open
+        self._tx_xfer()
+        self._rx_xfer()
+
+    def _on_rx(self, _):
+        # Receive MIDI events. Called via micropython.schedule, outside of the USB callback function.
+        m = self._rx.pend_read()
+        i = 0
+        while i <= len(m) - 4:
+            cin = m[i] & 0x0F
+            self.on_midi_event(cin, m[i + 1], m[i + 2], m[i + 3])
+            i += 4
+        self._rx.finish_read(i)
+
+    def desc_cfg(self, desc, itf_num, ep_num, strs):
+        # Start by registering a USB Audio Control interface, that is required to point to the
+        # actual MIDI interface
+        desc.interface(itf_num, 0, _INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_CONTROL)
+
+        # Append the class-specific AudioControl interface descriptor
+        desc.pack(
+            "<BBBHHBB",
+            9,  # bLength
+            0x24,  # bDescriptorType CS_INTERFACE
+            0x01,  # bDescriptorSubtype MS_HEADER
+            0x0100,  # BcdADC
+            0x0009,  # wTotalLength
+            0x01,  # bInCollection,
+            itf_num + 1,  # baInterfaceNr - points to the MIDI Streaming interface
+        )
+
+        # Next add the MIDI Streaming interface descriptor
+        desc.interface(
+            itf_num + 1, 2, _INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_MIDISTREAMING
+        )
+
+        # Append the class-specific interface descriptors
+
+        # Midi Streaming interface descriptor
+        desc.pack(
+            "<BBBHH",
+            7,  # bLength
+            0x24,  # bDescriptorType CS_INTERFACE
+            0x01,  # bDescriptorSubtype MS_HEADER
+            0x0100,  # BcdADC
+            # wTotalLength: of all class-specific descriptors
+            7
+            + 2
+            * (
+                _JACK_IN_DESC_LEN
+                + _JACK_OUT_DESC_LEN
+                + _STD_DESC_AUDIO_ENDPOINT_LEN
+                + _CLASS_DESC_ENDPOINT_LEN
+            ),
+        )
+
+        # The USB MIDI standard 1.0 allows modelling a baffling range of MIDI
+        # devices with different permutations of Jack descriptors, with a lot of
+        # scope for indicating internal connections in the device (as
+        # "virtualised" by the USB MIDI standard). Much of the options don't
+        # really change the USB behaviour but provide metadata to the host.
+        #
+        # As observed elsewhere online, the standard ends up being pretty
+        # complex and unclear in parts, but there is a clear simple example in
+        # an Appendix. So nearly everyone implements the device from the
+        # Appendix as-is, even when it's not a good fit for their application,
+        # and ignores the rest of the standard.
+        #
+        # For now, this is what this class does as well.
+
+        _jack_in_desc(desc, _JACK_TYPE_EMBEDDED, _EMB_IN_JACK_ID)
+        _jack_in_desc(desc, _JACK_TYPE_EXTERNAL, _EXT_IN_JACK_ID)
+        _jack_out_desc(desc, _JACK_TYPE_EMBEDDED, _EMB_OUT_JACK_ID, _EXT_IN_JACK_ID, 1)
+        _jack_out_desc(desc, _JACK_TYPE_EXTERNAL, _EXT_OUT_JACK_ID, _EMB_IN_JACK_ID, 1)
+
+        # One MIDI endpoint in each direction, plus the
+        # associated CS descriptors
+
+        self.ep_out = ep_num
+        self.ep_in = ep_num | _EP_IN_FLAG
+
+        # rx side, USB "in" endpoint and embedded MIDI IN Jacks
+        _audio_endpoint(desc, self.ep_in, _EMB_OUT_JACK_ID)
+
+        # tx side, USB "out" endpoint and embedded MIDI OUT jacks
+        _audio_endpoint(desc, self.ep_out, _EMB_IN_JACK_ID)
+
+    def num_itfs(self):
+        return 2
+
+    def num_eps(self):
+        return 1
+
+
+def _jack_in_desc(desc, bJackType, bJackID):
+    # Utility function appends a "JACK IN" descriptor with
+    # specified bJackType and bJackID
+    desc.pack(
+        "<BBBBBB",
+        _JACK_IN_DESC_LEN,  # bLength
+        0x24,  # bDescriptorType CS_INTERFACE
+        0x02,  # bDescriptorSubtype MIDI_IN_JACK
+        bJackType,
+        bJackID,
+        0x00,  # iJack, no string descriptor support yet
+    )
+
+
+def _jack_out_desc(desc, bJackType, bJackID, bSourceId, bSourcePin):
+    # Utility function appends a "JACK IN" descriptor with
+    # specified bJackType and bJackID
+    desc.pack(
+        "<BBBBBBBBB",
+        _JACK_OUT_DESC_LEN,  # bLength
+        0x24,  # bDescriptorType CS_INTERFACE
+        0x03,  # bDescriptorSubtype MIDI_OUT_JACK
+        bJackType,
+        bJackID,
+        0x01,  # bNrInputPins
+        bSourceId,  # baSourceID(1)
+        bSourcePin,  # baSourcePin(1)
+        0x00,  # iJack, no string descriptor support yet
+    )
+
+
+def _audio_endpoint(desc, bEndpointAddress, emb_jack_id):
+    # Append a standard USB endpoint descriptor and the USB class endpoint descriptor
+    # for this endpoint.
+    #
+    # Audio Class devices extend the standard endpoint descriptor with two extra bytes,
+    # so we can't easily call desc.endpoint() for the first part.
+    desc.pack(
+        # Standard USB endpoint descriptor (plus audio tweaks)
+        "<BBBBHBBB"
+        # Class endpoint descriptor
+        "BBBBB",
+        _STD_DESC_AUDIO_ENDPOINT_LEN,  # wLength
+        _STD_DESC_ENDPOINT_TYPE,  # bDescriptorType
+        bEndpointAddress,
+        2,  # bmAttributes, bulk
+        64,  # wMaxPacketSize
+        0,  # bInterval
+        0,  # bRefresh (unused)
+        0,  # bSynchInterval (unused)
+        _CLASS_DESC_ENDPOINT_LEN,  # bLength
+        0x25,  # bDescriptorType CS_ENDPOINT
+        0x01,  # bDescriptorSubtype MS_GENERAL
+        1,  # bNumEmbMIDIJack
+        emb_jack_id,  # BaAssocJackID(1)
+    )
diff --git a/micropython/usb/usb-device-mouse/manifest.py b/micropython/usb/usb-device-mouse/manifest.py
new file mode 100644
index 000000000..923535c4c
--- /dev/null
+++ b/micropython/usb/usb-device-mouse/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.0")
+require("usb-device-hid")
+package("usb")
diff --git a/micropython/usb/usb-device-mouse/usb/device/mouse.py b/micropython/usb/usb-device-mouse/usb/device/mouse.py
new file mode 100644
index 000000000..d3dea9c61
--- /dev/null
+++ b/micropython/usb/usb-device-mouse/usb/device/mouse.py
@@ -0,0 +1,100 @@
+# MicroPython USB Mouse module
+#
+# MIT license; Copyright (c) 2023-2024 Angus Gratton
+from micropython import const
+import struct
+import machine
+
+from usb.device.hid import HIDInterface
+
+_INTERFACE_PROTOCOL_MOUSE = const(0x02)
+
+
+class MouseInterface(HIDInterface):
+    # A basic three button USB mouse HID interface
+    def __init__(self, interface_str="MicroPython Mouse"):
+        super().__init__(
+            _MOUSE_REPORT_DESC,
+            protocol=_INTERFACE_PROTOCOL_MOUSE,
+            interface_str=interface_str,
+        )
+        self._l = False  # Left button
+        self._m = False  # Middle button
+        self._r = False  # Right button
+        self._buf = bytearray(3)
+
+    def send_report(self, dx=0, dy=0):
+        b = 0
+        if self._l:
+            b |= 1 << 0
+        if self._r:
+            b |= 1 << 1
+        if self._m:
+            b |= 1 << 2
+
+        # Wait for any pending report to be sent to the host
+        # before updating contents of _buf.
+        #
+        # This loop can be removed if you don't care about possibly missing a
+        # transient report, the final report buffer contents will always be the
+        # last one sent to the host (it just might lose one of the ones in the
+        # middle).
+        while self.busy():
+            machine.idle()
+
+        struct.pack_into("Bbb", self._buf, 0, b, dx, dy)
+
+        return super().send_report(self._buf)
+
+    def click_left(self, down=True):
+        self._l = down
+        return self.send_report()
+
+    def click_middle(self, down=True):
+        self._m = down
+        return self.send_report()
+
+    def click_right(self, down=True):
+        self._r = down
+        return self.send_report()
+
+    def move_by(self, dx, dy):
+        if not -127 <= dx <= 127:
+            raise ValueError("dx")
+        if not -127 <= dy <= 127:
+            raise ValueError("dy")
+        return self.send_report(dx, dy)
+
+
+# Basic 3-button mouse HID Report Descriptor.
+# This is based on Appendix E.10 of the HID v1.11 document.
+# fmt: off
+_MOUSE_REPORT_DESC = (
+    b'\x05\x01'  # Usage Page (Generic Desktop)
+        b'\x09\x02'  # Usage (Mouse)
+    b'\xA1\x01'  # Collection (Application)
+        b'\x09\x01'  # Usage (Pointer)
+        b'\xA1\x00'  # Collection (Physical)
+            b'\x05\x09'  # Usage Page (Buttons)
+                b'\x19\x01'  # Usage Minimum (01),
+                b'\x29\x03'  # Usage Maximun (03),
+                b'\x15\x00'  # Logical Minimum (0),
+                b'\x25\x01'  # Logical Maximum (1),
+                b'\x95\x03'  # Report Count (3),
+                b'\x75\x01'  # Report Size (1),
+                b'\x81\x02'  # Input (Data, Variable, Absolute), ;3 button bits
+                b'\x95\x01'  # Report Count (1),
+                b'\x75\x05'  # Report Size (5),
+                b'\x81\x01'  # Input (Constant), ;5 bit padding
+            b'\x05\x01'  # Usage Page (Generic Desktop),
+                b'\x09\x30'  # Usage (X),
+                b'\x09\x31'  # Usage (Y),
+                b'\x15\x81'  # Logical Minimum (-127),
+                b'\x25\x7F'  # Logical Maximum (127),
+                b'\x75\x08'  # Report Size (8),
+                b'\x95\x02'  # Report Count (2),
+                b'\x81\x06'  # Input (Data, Variable, Relative), ;2 position bytes (X & Y)
+        b'\xC0'  # End Collection
+    b'\xC0'  # End Collection
+)
+# fmt: on
diff --git a/micropython/usb/usb-device/manifest.py b/micropython/usb/usb-device/manifest.py
new file mode 100644
index 000000000..fa0d8a3f5
--- /dev/null
+++ b/micropython/usb/usb-device/manifest.py
@@ -0,0 +1,2 @@
+metadata(version="0.2.1")
+package("usb")
diff --git a/micropython/usb/usb-device/tests/test_core_buffer.py b/micropython/usb/usb-device/tests/test_core_buffer.py
new file mode 100644
index 000000000..633e618c7
--- /dev/null
+++ b/micropython/usb/usb-device/tests/test_core_buffer.py
@@ -0,0 +1,97 @@
+# Tests for the Buffer class included in usb.device.core
+#
+# The easiest way to run this is using unix port. From the parent usb-device
+# directory, run as:
+#
+# $ micropython -m tests.test_core_buffer
+#
+import micropython
+from usb.device import core
+
+if not hasattr(core.machine, "disable_irq"):
+    # Inject a fake machine module which allows testing on the unix port, and as
+    # a bonus have tests fail if the buffer allocates inside a critical section.
+    class FakeMachine:
+        def disable_irq(self):
+            return micropython.heap_lock()
+
+        def enable_irq(self, was_locked):
+            if not was_locked:
+                micropython.heap_unlock()
+
+    core.machine = FakeMachine()
+
+
+b = core.Buffer(16)
+
+# Check buffer is empty
+assert b.readable() == 0
+assert b.writable() == 16
+
+# Single write then read
+w = b.pend_write()
+assert len(w) == 16
+w[:8] = b"12345678"
+b.finish_write(8)
+
+# Empty again
+assert b.readable() == 8
+assert b.writable() == 8
+
+r = b.pend_read()
+assert len(r) == 8
+assert r == b"12345678"
+b.finish_read(8)
+
+# Empty buffer again
+assert b.readable() == 0
+assert b.writable() == 16
+
+# Single write then split reads
+b.write(b"abcdefghijklmnop")
+assert b.writable() == 0  # full buffer
+
+r = b.pend_read()
+assert r == b"abcdefghijklmnop"
+b.finish_read(2)
+
+r = b.pend_read()
+assert r == b"cdefghijklmnop"
+b.finish_read(3)
+
+# write to end of buffer
+b.write(b"AB")
+
+r = b.pend_read()
+assert r == b"fghijklmnopAB"
+
+# write while a read is pending
+b.write(b"XY")
+
+# previous pend_read() memoryview should be the same
+assert r == b"fghijklmnopAB"
+
+b.finish_read(4)
+r = b.pend_read()
+assert r == b"jklmnopABXY"  # four bytes consumed from head, one new byte at tail
+
+# read while a write is pending
+w = b.pend_write()
+assert len(w) == 5
+r = b.pend_read()
+assert len(r) == 11
+b.finish_read(3)
+w[:2] = b"12"
+b.finish_write(2)
+
+# Expected final state of buffer
+tmp = bytearray(b.readable())
+assert b.readinto(tmp) == len(tmp)
+assert tmp == b"mnopABXY12"
+
+# Now buffer is empty again
+assert b.readable() == 0
+assert b.readinto(tmp) == 0
+assert b.writable() == 16
+
+print("All Buffer tests passed")
diff --git a/micropython/usb/usb-device/usb/device/__init__.py b/micropython/usb/usb-device/usb/device/__init__.py
new file mode 100644
index 000000000..f77dad4f9
--- /dev/null
+++ b/micropython/usb/usb-device/usb/device/__init__.py
@@ -0,0 +1,2 @@
+from . import core
+from .core import get  # Singleton _Device getter
diff --git a/micropython/usb/usb-device/usb/device/core.py b/micropython/usb/usb-device/usb/device/core.py
new file mode 100644
index 000000000..b0d91d8ff
--- /dev/null
+++ b/micropython/usb/usb-device/usb/device/core.py
@@ -0,0 +1,885 @@
+# MicroPython Library runtime USB device implementation
+#
+# These contain the classes and utilities that are needed to
+# implement a USB device, not any complete USB drivers.
+#
+# MIT license; Copyright (c) 2022-2024 Angus Gratton
+from micropython import const
+import machine
+import struct
+
+try:
+    from _thread import get_ident
+except ImportError:
+
+    def get_ident():
+        return 0  # Placeholder, for no threading support
+
+
+_EP_IN_FLAG = const(1 << 7)
+
+# USB descriptor types
+_STD_DESC_DEV_TYPE = const(0x1)
+_STD_DESC_CONFIG_TYPE = const(0x2)
+_STD_DESC_STRING_TYPE = const(0x3)
+_STD_DESC_INTERFACE_TYPE = const(0x4)
+_STD_DESC_ENDPOINT_TYPE = const(0x5)
+_STD_DESC_INTERFACE_ASSOC = const(0xB)
+
+_ITF_ASSOCIATION_DESC_TYPE = const(0xB)  # Interface Association descriptor
+
+# Standard USB descriptor lengths
+_STD_DESC_CONFIG_LEN = const(9)
+_STD_DESC_ENDPOINT_LEN = const(7)
+_STD_DESC_INTERFACE_LEN = const(9)
+
+_DESC_OFFSET_LEN = const(0)
+_DESC_OFFSET_TYPE = const(1)
+
+_DESC_OFFSET_INTERFACE_NUM = const(2)  # for _STD_DESC_INTERFACE_TYPE
+_DESC_OFFSET_ENDPOINT_NUM = const(2)  # for _STD_DESC_ENDPOINT_TYPE
+
+# Standard control request bmRequest fields, can extract by calling split_bmRequestType()
+_REQ_RECIPIENT_DEVICE = const(0x0)
+_REQ_RECIPIENT_INTERFACE = const(0x1)
+_REQ_RECIPIENT_ENDPOINT = const(0x2)
+_REQ_RECIPIENT_OTHER = const(0x3)
+
+# Offsets into the standard configuration descriptor, to fixup
+_OFFS_CONFIG_iConfiguration = const(6)
+
+_INTERFACE_CLASS_VENDOR = const(0xFF)
+_INTERFACE_SUBCLASS_NONE = const(0x00)
+_PROTOCOL_NONE = const(0x00)
+
+# These need to match the constants in tusb_config.h
+_USB_STR_MANUF = const(0x01)
+_USB_STR_PRODUCT = const(0x02)
+_USB_STR_SERIAL = const(0x03)
+
+# Error constant to match mperrno.h
+_MP_EINVAL = const(22)
+
+_dev = None  # Singleton _Device instance
+
+
+def get():
+    # Getter to access the singleton instance of the
+    # MicroPython _Device object
+    #
+    # (note this isn't the low-level machine.USBDevice object, the low-level object is
+    # get()._usbd.)
+    global _dev
+    if not _dev:
+        _dev = _Device()
+    return _dev
+
+
+class _Device:
+    # Class that implements the Python parts of the MicroPython USBDevice.
+    #
+    # This class should only be instantiated by the singleton getter
+    # function usb.device.get(), never directly.
+    def __init__(self):
+        self._itfs = {}  # Mapping from interface number to interface object, set by init()
+        self._eps = {}  # Mapping from endpoint address to interface object, set by _open_cb()
+        self._ep_cbs = {}  # Mapping from endpoint address to Optional[xfer callback]
+        self._cb_thread = None  # Thread currently running endpoint callback
+        self._cb_ep = None  # Endpoint number currently running callback
+        self._usbd = machine.USBDevice()  # low-level API
+
+    def init(self, *itfs, **kwargs):
+        # Helper function to configure the USB device and activate it in a single call
+        self.active(False)
+        self.config(*itfs, **kwargs)
+        self.active(True)
+
+    def config(  # noqa: PLR0913
+        self,
+        *itfs,
+        builtin_driver=False,
+        manufacturer_str=None,
+        product_str=None,
+        serial_str=None,
+        configuration_str=None,
+        id_vendor=None,
+        id_product=None,
+        bcd_device=None,
+        device_class=0,
+        device_subclass=0,
+        device_protocol=0,
+        config_str=None,
+        max_power_ma=None,
+        remote_wakeup=False,
+    ):
+        # Configure the USB device with a set of interfaces, and optionally reconfiguring the
+        # device and configuration descriptor fields
+
+        _usbd = self._usbd
+
+        if self.active():
+            raise OSError(_MP_EINVAL)  # Must set active(False) first
+
+        # Convenience: Allow builtin_driver to be True, False or one of
+        # the machine.USBDevice.BUILTIN_ constants
+        if isinstance(builtin_driver, bool):
+            builtin_driver = _usbd.BUILTIN_DEFAULT if builtin_driver else _usbd.BUILTIN_NONE
+        _usbd.builtin_driver = builtin_driver
+
+        # Putting None for any strings that should fall back to the "built-in" value
+        # Indexes in this list depends on _USB_STR_MANUF, _USB_STR_PRODUCT, _USB_STR_SERIAL
+        strs = [None, manufacturer_str, product_str, serial_str]
+
+        # Build the device descriptor
+        FMT = "<BBHBBBBHHHBBBB"
+        # read the static descriptor fields
+        f = struct.unpack(FMT, builtin_driver.desc_dev)
+
+        def maybe_set(value, idx):
+            # Override a numeric descriptor value or keep builtin value f[idx] if 'value' is None
+            if value is not None:
+                return value
+            return f[idx]
+
+        # Either copy each descriptor field directly from the builtin device descriptor, or 'maybe'
+        # set it to the custom value from the object
+        desc_dev = struct.pack(
+            FMT,
+            f[0],  # bLength
+            f[1],  # bDescriptorType
+            f[2],  # bcdUSB
+            device_class,  # bDeviceClass
+            device_subclass,  # bDeviceSubClass
+            device_protocol,  # bDeviceProtocol
+            f[6],  # bMaxPacketSize0, TODO: allow overriding this value?
+            maybe_set(id_vendor, 7),  # idVendor
+            maybe_set(id_product, 8),  # idProduct
+            maybe_set(bcd_device, 9),  # bcdDevice
+            _USB_STR_MANUF,  # iManufacturer
+            _USB_STR_PRODUCT,  # iProduct
+            _USB_STR_SERIAL,  # iSerialNumber
+            1,
+        )  # bNumConfigurations
+
+        # Iterate interfaces to build the configuration descriptor
+
+        # Keep track of the interface and endpoint indexes
+        itf_num = builtin_driver.itf_max
+        ep_num = max(builtin_driver.ep_max, 1)  # Endpoint 0 always reserved for control
+        while len(strs) < builtin_driver.str_max:
+            strs.append(None)  # Reserve other string indexes used by builtin drivers
+        initial_cfg = builtin_driver.desc_cfg or (b"\x00" * _STD_DESC_CONFIG_LEN)
+
+        self._itfs = {}
+
+        # Determine the total length of the configuration descriptor, by making dummy
+        # calls to build the config descriptor
+        desc = Descriptor(None)
+        desc.extend(initial_cfg)
+        for itf in itfs:
+            itf.desc_cfg(desc, 0, 0, [])
+
+        # Allocate the real Descriptor helper to write into it, starting
+        # after the standard configuration descriptor
+        desc = Descriptor(bytearray(desc.o))
+        desc.extend(initial_cfg)
+        for itf in itfs:
+            itf.desc_cfg(desc, itf_num, ep_num, strs)
+
+            for _ in range(itf.num_itfs()):
+                self._itfs[itf_num] = itf  # Mapping from interface numbers to interfaces
+                itf_num += 1
+
+            ep_num += itf.num_eps()
+
+        # Go back and update the Standard Configuration Descriptor
+        # header at the start with values based on the complete
+        # descriptor.
+        #
+        # See USB 2.0 specification section 9.6.3 p264 for details.
+        bmAttributes = (
+            (1 << 7)  # Reserved
+            | (0 if max_power_ma else (1 << 6))  # Self-Powered
+            | ((1 << 5) if remote_wakeup else 0)
+        )
+
+        # Configuration string is optional but supported
+        iConfiguration = 0
+        if configuration_str:
+            iConfiguration = len(strs)
+            strs.append(configuration_str)
+
+        if max_power_ma is not None:
+            # Convert from mA to the units used in the descriptor
+            max_power_ma //= 2
+        else:
+            try:
+                # Default to whatever value the builtin driver reports
+                max_power_ma = _usbd.BUILTIN_DEFAULT.desc_cfg[8]
+            except IndexError:
+                # If no built-in driver, default to 250mA
+                max_power_ma = 125
+
+        desc.pack_into(
+            "<BBHBBBBB",
+            0,
+            _STD_DESC_CONFIG_LEN,  # bLength
+            _STD_DESC_CONFIG_TYPE,  # bDescriptorType
+            len(desc.b),  # wTotalLength
+            itf_num,
+            1,  # bConfigurationValue
+            iConfiguration,
+            bmAttributes,
+            max_power_ma,
+        )
+
+        _usbd.config(
+            desc_dev,
+            desc.b,
+            strs,
+            self._open_itf_cb,
+            self._reset_cb,
+            self._control_xfer_cb,
+            self._xfer_cb,
+        )
+
+    def active(self, *optional_value):
+        # Thin wrapper around the USBDevice active() function.
+        #
+        # Note: active only means the USB device is available, not that it has
+        # actually been connected to and configured by a USB host. Use the
+        # Interface.is_open() function to check if the host has configured an
+        # interface of the device.
+        return self._usbd.active(*optional_value)
+
+    def _open_itf_cb(self, desc):
+        # Callback from TinyUSB lower layer, when USB host does Set
+        # Configuration. Called once per interface or IAD.
+
+        # Note that even if the configuration descriptor contains an IAD, 'desc'
+        # starts from the first interface descriptor in the IAD and not the IAD
+        # descriptor.
+
+        itf_num = desc[_DESC_OFFSET_INTERFACE_NUM]
+        itf = self._itfs[itf_num]
+
+        # Scan the full descriptor:
+        # - Build _eps and _ep_addr from the endpoint descriptors
+        # - Find the highest numbered interface provided to the callback
+        #   (which will be the first interface, unless we're scanning
+        #   multiple interfaces inside an IAD.)
+        offs = 0
+        max_itf = itf_num
+        while offs < len(desc):
+            dl = desc[offs + _DESC_OFFSET_LEN]
+            dt = desc[offs + _DESC_OFFSET_TYPE]
+            if dt == _STD_DESC_ENDPOINT_TYPE:
+                ep_addr = desc[offs + _DESC_OFFSET_ENDPOINT_NUM]
+                self._eps[ep_addr] = itf
+                self._ep_cbs[ep_addr] = None
+            elif dt == _STD_DESC_INTERFACE_TYPE:
+                max_itf = max(max_itf, desc[offs + _DESC_OFFSET_INTERFACE_NUM])
+            offs += dl
+
+        # If 'desc' is not the inside of an Interface Association Descriptor but
+        # 'itf' object still represents multiple USB interfaces (i.e. MIDI),
+        # defer calling 'itf.on_open()' until this callback fires for the
+        # highest numbered USB interface.
+        #
+        # This means on_open() is only called once, and that it can
+        # safely submit transfers on any of the USB interfaces' endpoints.
+        if self._itfs.get(max_itf + 1, None) != itf:
+            itf.on_open()
+
+    def _reset_cb(self):
+        # TinyUSB lower layer callback when the USB device is reset by the host
+
+        # Allow interfaces to respond to the reset
+        for itf in self._itfs.values():
+            itf.on_reset()
+
+        # Rebuilt when host re-enumerates
+        self._eps = {}
+        self._ep_cbs = {}
+
+    def _submit_xfer(self, ep_addr, data, done_cb=None):
+        # Submit a USB transfer (of any type except control) to TinyUSB lower layer.
+        #
+        # Generally, drivers should call Interface.submit_xfer() instead. See
+        # that function for documentation about the possible parameter values.
+        if ep_addr not in self._eps:
+            raise ValueError("ep_addr")
+        if self._xfer_pending(ep_addr):
+            raise RuntimeError("xfer_pending")
+
+        # USBDevice callback may be called immediately, before Python execution
+        # continues, so set it first.
+        #
+        # To allow xfer_pending checks to work, store True instead of None.
+        self._ep_cbs[ep_addr] = done_cb or True
+        return self._usbd.submit_xfer(ep_addr, data)
+
+    def _xfer_pending(self, ep_addr):
+        # Returns True if a transfer is pending on this endpoint.
+        #
+        # Generally, drivers should call Interface.xfer_pending() instead. See that
+        # function for more documentation.
+        return self._ep_cbs[ep_addr] or (self._cb_ep == ep_addr and self._cb_thread != get_ident())
+
+    def _xfer_cb(self, ep_addr, result, xferred_bytes):
+        # Callback from TinyUSB lower layer when a transfer completes.
+        cb = self._ep_cbs.get(ep_addr, None)
+        self._cb_thread = get_ident()
+        self._cb_ep = ep_addr  # Track while callback is running
+        self._ep_cbs[ep_addr] = None
+
+        # In most cases, 'cb' is a callback function for the transfer. Can also be:
+        # - True (for a transfer with no callback)
+        # - None (TinyUSB callback arrived for invalid endpoint, or no transfer.
+        #   Generally unlikely, but may happen in transient states.)
+        try:
+            if callable(cb):
+                cb(ep_addr, result, xferred_bytes)
+        finally:
+            self._cb_ep = None
+
+    def _control_xfer_cb(self, stage, request):
+        # Callback from TinyUSB lower layer when a control
+        # transfer is in progress.
+        #
+        # stage determines appropriate responses (possible values
+        # utils.STAGE_SETUP, utils.STAGE_DATA, utils.STAGE_ACK).
+        #
+        # The TinyUSB class driver framework only calls this function for
+        # particular types of control transfer, other standard control transfers
+        # are handled by TinyUSB itself.
+        wIndex = request[4] + (request[5] << 8)
+        recipient, _, _ = split_bmRequestType(request[0])
+
+        itf = None
+        result = None
+
+        if recipient == _REQ_RECIPIENT_DEVICE:
+            itf = self._itfs.get(wIndex & 0xFFFF, None)
+            if itf:
+                result = itf.on_device_control_xfer(stage, request)
+        elif recipient == _REQ_RECIPIENT_INTERFACE:
+            itf = self._itfs.get(wIndex & 0xFFFF, None)
+            if itf:
+                result = itf.on_interface_control_xfer(stage, request)
+        elif recipient == _REQ_RECIPIENT_ENDPOINT:
+            ep_num = wIndex & 0xFFFF
+            itf = self._eps.get(ep_num, None)
+            if itf:
+                result = itf.on_endpoint_control_xfer(stage, request)
+
+        if not itf:
+            # At time this code was written, only the control transfers shown
+            # above are passed to the class driver callback. See
+            # invoke_class_control() in tinyusb usbd.c
+            raise RuntimeError(f"Unexpected control request type {request[0]:#x}")
+
+        # Expecting any of the following possible replies from
+        # on_NNN_control_xfer():
+        #
+        # True - Continue transfer, no data
+        # False - STALL transfer
+        # Object with buffer interface - submit this data for the control transfer
+        return result
+
+
+class Interface:
+    # Abstract base class to implement USB Interface (and associated endpoints),
+    # or a collection of USB Interfaces, in Python
+    #
+    # (Despite the name an object of type Interface can represent multiple
+    # associated interfaces, with or without an Interface Association Descriptor
+    # prepended to them. Override num_itfs() if assigning >1 USB interface.)
+
+    def __init__(self):
+        self._open = False
+
+    def desc_cfg(self, desc, itf_num, ep_num, strs):
+        # Function to build configuration descriptor contents for this interface
+        # or group of interfaces. This is called on each interface from
+        # USBDevice.init().
+        #
+        # This function should insert:
+        #
+        # - At least one standard Interface descriptor (can call
+        # - desc.interface()).
+        #
+        # Plus, optionally:
+        #
+        # - One or more endpoint descriptors (can call desc.endpoint()).
+        # - An Interface Association Descriptor, prepended before.
+        # - Other class-specific configuration descriptor data.
+        #
+        # This function is called twice per call to USBDevice.init(). The first
+        # time the values of all arguments are dummies that are used only to
+        # calculate the total length of the descriptor. Therefore, anything this
+        # function does should be idempotent and it should add the same
+        # descriptors each time. If saving interface numbers or endpoint numbers
+        # for later
+        #
+        # Parameters:
+        #
+        # - desc - Descriptor helper to write the configuration descriptor bytes into.
+        #   The first time this function is called 'desc' is a dummy object
+        #   with no backing buffer (exists to count the number of bytes needed).
+        #
+        # - itf_num - First bNumInterfaces value to assign. The descriptor
+        #   should contain the same number of interfaces returned by num_itfs(),
+        #   starting from this value.
+        #
+        # - ep_num - Address of the first available endpoint number to use for
+        #   endpoint descriptor addresses. Subclasses should save the
+        #   endpoint addresses selected, to look up later (although note the first
+        #   time this function is called, the values will be dummies.)
+        #
+        # - strs - list of string descriptors for this USB device. This function
+        #   can append to this list, and then insert the index of the new string
+        #   in the list into the configuration descriptor.
+        raise NotImplementedError
+
+    def num_itfs(self):
+        # Return the number of actual USB Interfaces represented by this object
+        # (as set in desc_cfg().)
+        #
+        # Only needs to be overriden if implementing a Interface class that
+        # represents more than one USB Interface descriptor (i.e. MIDI), or an
+        # Interface Association Descriptor (i.e. USB-CDC).
+        return 1
+
+    def num_eps(self):
+        # Return the number of USB Endpoint numbers represented by this object
+        # (as set in desc_cfg().)
+        #
+        # Note for each count returned by this function, the interface may
+        # choose to have both an IN and OUT endpoint (i.e. IN flag is not
+        # considered a value here.)
+        #
+        # This value can be zero, if the USB Host only communicates with this
+        # interface using control transfers.
+        return 0
+
+    def on_open(self):
+        # Callback called when the USB host accepts the device configuration.
+        #
+        # Override this function to initiate any operations that the USB interface
+        # should do when the USB device is configured to the host.
+        self._open = True
+
+    def on_reset(self):
+        # Callback called on every registered interface when the USB device is
+        # reset by the host. This can happen when the USB device is unplugged,
+        # or if the host triggers a reset for some other reason.
+        #
+        # Override this function to cancel any pending operations specific to
+        # the interface (outstanding USB transfers are already cancelled).
+        #
+        # At this point, no USB functionality is available - on_open() will
+        # be called later if/when the USB host re-enumerates and configures the
+        # interface.
+        self._open = False
+
+    def is_open(self):
+        # Returns True if the interface has been configured by the host and is in
+        # active use.
+        return self._open
+
+    def on_device_control_xfer(self, stage, request):
+        # Control transfer callback. Override to handle a non-standard device
+        # control transfer where bmRequestType Recipient is Device, Type is
+        # utils.REQ_TYPE_CLASS, and the lower byte of wIndex indicates this interface.
+        #
+        # (See USB 2.0 specification 9.4 Standard Device Requests, p250).
+        #
+        # This particular request type seems pretty uncommon for a device class
+        # driver to need to handle, most hosts will not send this so most
+        # implementations won't need to override it.
+        #
+        # Parameters:
+        #
+        # - stage is one of utils.STAGE_SETUP, utils.STAGE_DATA, utils.STAGE_ACK.
+        #
+        # - request is a memoryview into a USB request packet, as per USB 2.0
+        #   specification 9.3 USB Device Requests, p250.  the memoryview is only
+        #   valid while the callback is running.
+        #
+        # The function can call split_bmRequestType(request[0]) to split
+        # bmRequestType into (Recipient, Type, Direction).
+        #
+        # Result, any of:
+        #
+        # - True to continue the request, False to STALL the endpoint.
+        # - Buffer interface object to provide a buffer to the host as part of the
+        #   transfer, if applicable.
+        return False
+
+    def on_interface_control_xfer(self, stage, request):
+        # Control transfer callback. Override to handle a device control
+        # transfer where bmRequestType Recipient is Interface, and the lower byte
+        # of wIndex indicates this interface.
+        #
+        # (See USB 2.0 specification 9.4 Standard Device Requests, p250).
+        #
+        # bmRequestType Type field may have different values. It's not necessary
+        # to handle the mandatory Standard requests (bmRequestType Type ==
+        # utils.REQ_TYPE_STANDARD), if the driver returns False in these cases then
+        # TinyUSB will provide the necessary responses.
+        #
+        # See on_device_control_xfer() for a description of the arguments and
+        # possible return values.
+        return False
+
+    def on_endpoint_control_xfer(self, stage, request):
+        # Control transfer callback. Override to handle a device
+        # control transfer where bmRequestType Recipient is Endpoint and
+        # the lower byte of wIndex indicates an endpoint address associated
+        # with this interface.
+        #
+        # bmRequestType Type will generally have any value except
+        # utils.REQ_TYPE_STANDARD, as Standard endpoint requests are handled by
+        # TinyUSB. The exception is the the Standard "Set Feature" request. This
+        # is handled by Tiny USB but also passed through to the driver in case it
+        # needs to change any internal state, but most drivers can ignore and
+        # return False in this case.
+        #
+        # (See USB 2.0 specification 9.4 Standard Device Requests, p250).
+        #
+        # See on_device_control_xfer() for a description of the parameters and
+        # possible return values.
+        return False
+
+    def xfer_pending(self, ep_addr):
+        # Return True if a transfer is already pending on ep_addr.
+        #
+        # Only one transfer can be submitted at a time.
+        #
+        # The transfer is marked pending while a completion callback is running
+        # for that endpoint, unless this function is called from the callback
+        # itself. This makes it simple to submit a new transfer from the
+        # completion callback.
+        return _dev and _dev._xfer_pending(ep_addr)
+
+    def submit_xfer(self, ep_addr, data, done_cb=None):
+        # Submit a USB transfer (of any type except control)
+        #
+        # Parameters:
+        #
+        # - ep_addr. Address of the endpoint to submit the transfer on. Caller is
+        #   responsible for ensuring that ep_addr is correct and belongs to this
+        #   interface. Only one transfer can be active at a time on each endpoint.
+        #
+        # - data. Buffer containing data to send, or for data to be read into
+        #   (depending on endpoint direction).
+        #
+        # - done_cb. Optional callback function for when the transfer
+        # completes. The callback is called with arguments (ep_addr, result,
+        # xferred_bytes) where result is one of xfer_result_t enum (see top of
+        # this file), and xferred_bytes is an integer.
+        #
+        # If the function returns, the transfer is queued.
+        #
+        # The function will raise RuntimeError under the following conditions:
+        #
+        # - The interface is not "open" (i.e. has not been enumerated and configured
+        #   by the host yet.)
+        #
+        # - A transfer is already pending on this endpoint (use xfer_pending() to check
+        #   before sending if needed.)
+        #
+        # - A DCD error occurred when queueing the transfer on the hardware.
+        #
+        #
+        # Will raise TypeError if 'data' isn't he correct type of buffer for the
+        # endpoint transfer direction.
+        #
+        # Note that done_cb may be called immediately, possibly before this
+        # function has returned to the caller.
+        if not self._open:
+            raise RuntimeError("Not open")
+        if not _dev._submit_xfer(ep_addr, data, done_cb):
+            raise RuntimeError("DCD error")
+
+    def stall(self, ep_addr, *args):
+        # Set or get the endpoint STALL state.
+        #
+        # To get endpoint stall stage, call with a single argument.
+        # To set endpoint stall state, call with an additional boolean
+        # argument to set or clear.
+        #
+        # Generally endpoint STALL is handled automatically, but there are some
+        # device classes that need to explicitly stall or unstall an endpoint
+        # under certain conditions.
+        if not self._open or ep_addr not in self._eps:
+            raise RuntimeError
+        _dev._usbd.stall(ep_addr, *args)
+
+
+class Descriptor:
+    # Wrapper class for writing a descriptor in-place into a provided buffer
+    #
+    # Doesn't resize the buffer.
+    #
+    # Can be initialised with b=None to perform a dummy pass that calculates the
+    # length needed for the buffer.
+    def __init__(self, b):
+        self.b = b
+        self.o = 0  # offset of data written to the buffer
+
+    def pack(self, fmt, *args):
+        # Utility function to pack new data into the descriptor
+        # buffer, starting at the current offset.
+        #
+        # Arguments are the same as struct.pack(), but it fills the
+        # pre-allocated descriptor buffer (growing if needed), instead of
+        # returning anything.
+        self.pack_into(fmt, self.o, *args)
+
+    def pack_into(self, fmt, offs, *args):
+        # Utility function to pack new data into the descriptor at offset 'offs'.
+        #
+        # If the data written is before 'offs' then self.o isn't incremented,
+        # otherwise it's incremented to point at the end of the written data.
+        end = offs + struct.calcsize(fmt)
+        if self.b:
+            struct.pack_into(fmt, self.b, offs, *args)
+        self.o = max(self.o, end)
+
+    def extend(self, a):
+        # Extend the descriptor with some bytes-like data
+        if self.b:
+            self.b[self.o : self.o + len(a)] = a
+        self.o += len(a)
+
+    # TODO: At the moment many of these arguments are named the same as the relevant field
+    # in the spec, as this is easier to understand. Can save some code size by collapsing them
+    # down.
+
+    def interface(
+        self,
+        bInterfaceNumber,
+        bNumEndpoints,
+        bInterfaceClass=_INTERFACE_CLASS_VENDOR,
+        bInterfaceSubClass=_INTERFACE_SUBCLASS_NONE,
+        bInterfaceProtocol=_PROTOCOL_NONE,
+        iInterface=0,
+    ):
+        # Utility function to append a standard Interface descriptor, with
+        # the properties specified in the parameter list.
+        #
+        # Defaults for bInterfaceClass, SubClass and Protocol are a "vendor"
+        # device.
+        #
+        # Note that iInterface is a string index number. If set, it should be set
+        # by the caller Interface to the result of self._get_str_index(s),
+        # where 's' is a string found in self.strs.
+        self.pack(
+            "BBBBBBBBB",
+            _STD_DESC_INTERFACE_LEN,  # bLength
+            _STD_DESC_INTERFACE_TYPE,  # bDescriptorType
+            bInterfaceNumber,
+            0,  # bAlternateSetting, not currently supported
+            bNumEndpoints,
+            bInterfaceClass,
+            bInterfaceSubClass,
+            bInterfaceProtocol,
+            iInterface,
+        )
+
+    def endpoint(self, bEndpointAddress, bmAttributes, wMaxPacketSize, bInterval=1):
+        # Utility function to append a standard Endpoint descriptor, with
+        # the properties specified in the parameter list.
+        #
+        # See USB 2.0 specification section 9.6.6 Endpoint p269
+        #
+        # As well as a numeric value, bmAttributes can be a string value to represent
+        # common endpoint types: "control", "bulk", "interrupt".
+        if bmAttributes == "control":
+            bmAttributes = 0
+        elif bmAttributes == "bulk":
+            bmAttributes = 2
+        elif bmAttributes == "interrupt":
+            bmAttributes = 3
+
+        self.pack(
+            "<BBBBHB",
+            _STD_DESC_ENDPOINT_LEN,
+            _STD_DESC_ENDPOINT_TYPE,
+            bEndpointAddress,
+            bmAttributes,
+            wMaxPacketSize,
+            bInterval,
+        )
+
+    def interface_assoc(
+        self,
+        bFirstInterface,
+        bInterfaceCount,
+        bFunctionClass,
+        bFunctionSubClass,
+        bFunctionProtocol=_PROTOCOL_NONE,
+        iFunction=0,
+    ):
+        # Utility function to append an Interface Association descriptor,
+        # with the properties specified in the parameter list.
+        #
+        # See USB ECN: Interface Association Descriptor.
+        self.pack(
+            "<BBBBBBBB",
+            8,
+            _ITF_ASSOCIATION_DESC_TYPE,
+            bFirstInterface,
+            bInterfaceCount,
+            bFunctionClass,
+            bFunctionSubClass,
+            bFunctionProtocol,
+            iFunction,
+        )
+
+
+def split_bmRequestType(bmRequestType):
+    # Utility function to split control transfer field bmRequestType into a tuple of 3 fields:
+    #
+    # Recipient
+    # Type
+    # Data transfer direction
+    #
+    # See USB 2.0 specification section 9.3 USB Device Requests and 9.3.1 bmRequestType, p248.
+    return (
+        bmRequestType & 0x1F,
+        (bmRequestType >> 5) & 0x03,
+        (bmRequestType >> 7) & 0x01,
+    )
+
+
+class Buffer:
+    # An interrupt-safe producer/consumer buffer that wraps a bytearray object.
+    #
+    # Kind of like a ring buffer, but supports the idea of returning a
+    # memoryview for either read or write of multiple bytes (suitable for
+    # passing to a buffer function without needing to allocate another buffer to
+    # read into.)
+    #
+    # Consumer can call pend_read() to get a memoryview to read from, and then
+    # finish_read(n) when done to indicate it read 'n' bytes from the
+    # memoryview. There is also a readinto() convenience function.
+    #
+    # Producer must call pend_write() to get a memorybuffer to write into, and
+    # then finish_write(n) when done to indicate it wrote 'n' bytes into the
+    # memoryview. There is also a normal write() convenience function.
+    #
+    # - Only one producer and one consumer is supported.
+    #
+    # - Calling pend_read() and pend_write() is effectively idempotent, they can be
+    #   called more than once without a corresponding finish_x() call if necessary
+    #   (provided only one thread does this, as per the previous point.)
+    #
+    # - Calling finish_write() and finish_read() is hard interrupt safe (does
+    #   not allocate). pend_read() and pend_write() each allocate 1 block for
+    #   the memoryview that is returned.
+    #
+    # The buffer contents are always laid out as:
+    #
+    # - Slice [:_n] = bytes of valid data waiting to read
+    # - Slice [_n:_w] = unused space
+    # - Slice [_w:] = bytes of pending write buffer waiting to be written
+    #
+    # This buffer should be fast when most reads and writes are balanced and use
+    # the whole buffer. When this doesn't happen, performance degrades to
+    # approximate a Python-based single byte ringbuffer.
+    #
+    def __init__(self, length):
+        self._b = memoryview(bytearray(length))
+        # number of bytes in buffer read to read, starting at index 0. Updated
+        # by both producer & consumer.
+        self._n = 0
+        # start index of a pending write into the buffer, if any. equals
+        # len(self._b) if no write is pending. Updated by producer only.
+        self._w = length
+
+    def writable(self):
+        # Number of writable bytes in the buffer. Assumes no pending write is outstanding.
+        return len(self._b) - self._n
+
+    def readable(self):
+        # Number of readable bytes in the buffer. Assumes no pending read is outstanding.
+        return self._n
+
+    def pend_write(self, wmax=None):
+        # Returns a memoryview that the producer can write bytes into.
+        # start the write at self._n, the end of data waiting to read
+        #
+        # If wmax is set then the memoryview is pre-sliced to be at most
+        # this many bytes long.
+        #
+        # (No critical section needed as self._w is only updated by the producer.)
+        self._w = self._n
+        end = (self._w + wmax) if wmax else len(self._b)
+        return self._b[self._w : end]
+
+    def finish_write(self, nbytes):
+        # Called by the producer to indicate it wrote nbytes into the buffer.
+        ist = machine.disable_irq()
+        try:
+            assert nbytes <= len(self._b) - self._w  # can't say we wrote more than was pended
+            if self._n == self._w:
+                # no data was read while the write was happening, so the buffer is already in place
+                # (this is the fast path)
+                self._n += nbytes
+            else:
+                # Slow path: data was read while the write was happening, so
+                # shuffle the newly written bytes back towards index 0 to avoid fragmentation
+                #
+                # As this updates self._n we have to do it in the critical
+                # section, so do it byte by byte to avoid allocating.
+                while nbytes > 0:
+                    self._b[self._n] = self._b[self._w]
+                    self._n += 1
+                    self._w += 1
+                    nbytes -= 1
+
+            self._w = len(self._b)
+        finally:
+            machine.enable_irq(ist)
+
+    def write(self, w):
+        # Helper method for the producer to write into the buffer in one call
+        pw = self.pend_write()
+        to_w = min(len(w), len(pw))
+        if to_w:
+            pw[:to_w] = w[:to_w]
+            self.finish_write(to_w)
+        return to_w
+
+    def pend_read(self):
+        # Return a memoryview slice that the consumer can read bytes from
+        return self._b[: self._n]
+
+    def finish_read(self, nbytes):
+        # Called by the consumer to indicate it read nbytes from the buffer.
+        if not nbytes:
+            return
+        ist = machine.disable_irq()
+        try:
+            assert nbytes <= self._n  # can't say we read more than was available
+            i = 0
+            self._n -= nbytes
+            while i < self._n:
+                # consumer only read part of the buffer, so shuffle remaining
+                # read data back towards index 0 to avoid fragmentation
+                self._b[i] = self._b[i + nbytes]
+                i += 1
+        finally:
+            machine.enable_irq(ist)
+
+    def readinto(self, b):
+        # Helper method for the consumer to read out of the buffer in one call
+        pr = self.pend_read()
+        to_r = min(len(pr), len(b))
+        if to_r:
+            b[:to_r] = pr[:to_r]
+            self.finish_read(to_r)
+        return to_r
diff --git a/micropython/utop/README.md b/micropython/utop/README.md
new file mode 100644
index 000000000..7b07e785d
--- /dev/null
+++ b/micropython/utop/README.md
@@ -0,0 +1,12 @@
+# utop
+
+Provides a top-like live overview of the running system.
+
+On the `esp32` port this depends on the `esp32.idf_task_info()` function, which
+can be enabled by adding the following lines to the board `sdkconfig`:
+
+```ini
+CONFIG_FREERTOS_USE_TRACE_FACILITY=y
+CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y
+CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
+```
diff --git a/micropython/utop/manifest.py b/micropython/utop/manifest.py
new file mode 100644
index 000000000..ebba07270
--- /dev/null
+++ b/micropython/utop/manifest.py
@@ -0,0 +1,6 @@
+metadata(
+    version="0.1.0",
+    description="Provides a top-like live overview of the running system.",
+)
+
+module("utop.py")
diff --git a/micropython/utop/utop.py b/micropython/utop/utop.py
new file mode 100644
index 000000000..799ff4212
--- /dev/null
+++ b/micropython/utop/utop.py
@@ -0,0 +1,109 @@
+import micropython
+import time
+
+try:
+    import esp32
+    import _thread
+except ImportError:
+    esp32 = None
+
+
+def top(update_interval_ms=1000, timeout_ms=None, thread_names={}):
+    time_start = time.ticks_ms()
+    previous_total_runtime = None
+    previous_task_runtimes = {}
+    previous_line_count = 0
+    esp32_task_state_names = dict(
+        enumerate(("running", "ready", "blocked", "suspended", "deleted", "invalid"))
+    )
+
+    while timeout_ms is None or abs(time.ticks_diff(time.ticks_ms(), time_start)) < timeout_ms:
+        if previous_line_count > 0:
+            print("\x1b[{}A".format(previous_line_count), end="")
+        line_count = 0
+
+        if esp32 is not None:
+            if not hasattr(esp32, "idf_task_info"):
+                print(
+                    "INFO: esp32.idf_task_info() is not available, cannot list active tasks.\x1b[K"
+                )
+                line_count += 1
+            else:
+                print("   CPU%  CORE  PRIORITY  STATE      STACKWATERMARK  NAME\x1b[K")
+                line_count += 1
+
+                total_runtime, tasks = esp32.idf_task_info()
+                current_thread_id = _thread.get_ident()
+                tasks.sort(key=lambda t: t[0])
+                for (
+                    task_id,
+                    task_name,
+                    task_state,
+                    task_priority,
+                    task_runtime,
+                    task_stackhighwatermark,
+                    task_coreid,
+                ) in tasks:
+                    task_runtime_percentage = "-"
+                    if (
+                        total_runtime != previous_total_runtime
+                        and task_id in previous_task_runtimes
+                    ):
+                        task_runtime_percentage = "{:.2f}%".format(
+                            100
+                            * ((task_runtime - previous_task_runtimes[task_id]) & (2**32 - 1))
+                            / ((total_runtime - previous_total_runtime) & (2**32 - 1))
+                        )
+                    print(
+                        "{:>7}  {:>4}  {:>8d}  {:<9}  {:<14d}  {}{}\x1b[K".format(
+                            task_runtime_percentage,
+                            "-" if task_coreid is None else task_coreid,
+                            task_priority,
+                            esp32_task_state_names.get(task_state, "unknown"),
+                            task_stackhighwatermark,
+                            thread_names.get(task_id, task_name),
+                            " (*)" if task_id == current_thread_id else "",
+                        )
+                    )
+                    line_count += 1
+
+                    previous_task_runtimes[task_id] = task_runtime
+                previous_total_runtime = total_runtime
+        else:
+            print("INFO: Platform does not support listing active tasks.\x1b[K")
+            line_count += 1
+
+        print("\x1b[K")
+        line_count += 1
+        print("MicroPython ", end="")
+        micropython.mem_info()
+        line_count += 3
+
+        if esp32 is not None:
+            print("\x1b[K")
+            line_count += 1
+            for name, cap in (("data", esp32.HEAP_DATA), ("exec", esp32.HEAP_EXEC)):
+                heaps = esp32.idf_heap_info(cap)
+                print(
+                    "IDF heap ({}): {} regions, {} total, {} free, {} largest contiguous, {} min free watermark\x1b[K".format(
+                        name,
+                        len(heaps),
+                        sum((h[0] for h in heaps)),
+                        sum((h[1] for h in heaps)),
+                        max((h[2] for h in heaps)),
+                        sum((h[3] for h in heaps)),
+                    )
+                )
+                line_count += 1
+
+        if previous_line_count > line_count:
+            for _ in range(previous_line_count - line_count):
+                print("\x1b[K")
+            print("\x1b[{}A".format(previous_line_count - line_count), end="")
+
+        previous_line_count = line_count
+
+        try:
+            time.sleep_ms(update_interval_ms)
+        except KeyboardInterrupt:
+            break
diff --git a/micropython/xmltok/manifest.py b/micropython/xmltok/manifest.py
new file mode 100644
index 000000000..70d5556bf
--- /dev/null
+++ b/micropython/xmltok/manifest.py
@@ -0,0 +1,5 @@
+metadata(description="Simple XML tokenizer", version="0.2.1")
+
+# Originally written by Paul Sokolovsky.
+
+module("xmltok.py")
diff --git a/xmltok/test.xml b/micropython/xmltok/test.xml
similarity index 85%
rename from xmltok/test.xml
rename to micropython/xmltok/test.xml
index 1c3607f18..e13f548c8 100644
--- a/xmltok/test.xml
+++ b/micropython/xmltok/test.xml
@@ -9,5 +9,6 @@
   baz
   
 </u:GetConnectionTypeInfo>
+<!-- Comments, you won't see me after tokenize. -->
 </s:Body>
 </s:Envelope>
diff --git a/micropython/xmltok/test_xmltok.py b/micropython/xmltok/test_xmltok.py
new file mode 100644
index 000000000..98ec0d114
--- /dev/null
+++ b/micropython/xmltok/test_xmltok.py
@@ -0,0 +1,25 @@
+import xmltok
+
+expected = [
+    ("PI", "xml"),
+    ("ATTR", ("", "version"), "1.0"),
+    ("START_TAG", ("s", "Envelope")),
+    ("ATTR", ("xmlns", "s"), "http://schemas.xmlsoap.org/soap/envelope/"),
+    ("ATTR", ("s", "encodingStyle"), "http://schemas.xmlsoap.org/soap/encoding/"),
+    ("START_TAG", ("s", "Body")),
+    ("START_TAG", ("u", "GetConnectionTypeInfo")),
+    ("ATTR", ("xmlns", "u"), "urn:schemas-upnp-org:service:WANIPConnection:1"),
+    ("TEXT", "foo bar\n  baz\n  \n"),
+    ("END_TAG", ("u", "GetConnectionTypeInfo")),
+    ("END_TAG", ("s", "Body")),
+    ("END_TAG", ("s", "Envelope")),
+]
+
+dir = "."
+if "/" in __file__:
+    dir = __file__.rsplit("/", 1)[0]
+
+ex = iter(expected)
+for i in xmltok.tokenize(open(dir + "/test.xml")):
+    # print(i)
+    assert i == next(ex)
diff --git a/micropython/xmltok/xmltok.py b/micropython/xmltok/xmltok.py
new file mode 100644
index 000000000..9711b7ff2
--- /dev/null
+++ b/micropython/xmltok/xmltok.py
@@ -0,0 +1,148 @@
+TEXT = "TEXT"
+START_TAG = "START_TAG"
+# START_TAG_DONE = "START_TAG_DONE"
+END_TAG = "END_TAG"
+PI = "PI"
+# PI_DONE = "PI_DONE"
+ATTR = "ATTR"
+# ATTR_VAL = "ATTR_VAL"
+
+
+class XMLSyntaxError(Exception):
+    pass
+
+
+class XMLTokenizer:
+    def __init__(self, f):
+        self.f = f
+        self.nextch()
+
+    def curch(self):
+        return self.c
+
+    def getch(self):
+        c = self.c
+        self.nextch()
+        return c
+
+    def eof(self):
+        return self.c == ""
+
+    def nextch(self):
+        self.c = self.f.read(1)
+        if not self.c:
+            raise EOFError
+        return self.c
+
+    def skip_ws(self):
+        while self.curch().isspace():
+            self.nextch()
+
+    def isident(self):
+        self.skip_ws()
+        return self.curch().isalpha()
+
+    def getident(self):
+        self.skip_ws()
+        ident = ""
+        while True:
+            c = self.curch()
+            if not (c.isalpha() or c.isdigit() or c in "_-."):
+                break
+            ident += self.getch()
+        return ident
+
+    def getnsident(self):
+        ns = ""
+        ident = self.getident()
+        if self.curch() == ":":
+            self.nextch()
+            ns = ident
+            ident = self.getident()
+        return (ns, ident)
+
+    def match(self, c):
+        self.skip_ws()
+        if self.curch() == c:
+            self.nextch()
+            return True
+        return False
+
+    def expect(self, c):
+        if not self.match(c):
+            raise XMLSyntaxError
+
+    def lex_attrs_till(self):
+        while self.isident():
+            attr = self.getnsident()
+            # yield (ATTR, attr)
+            self.expect("=")
+            self.expect('"')
+            val = ""
+            while self.curch() != '"':
+                val += self.getch()
+            # yield (ATTR_VAL, val)
+            self.expect('"')
+            yield (ATTR, attr, val)
+
+    def tokenize(self):
+        while not self.eof():
+            try:
+                if self.match("<"):
+                    if self.match("/"):
+                        yield (END_TAG, self.getnsident())
+                        self.expect(">")
+                    elif self.match("?"):
+                        yield (PI, self.getident())
+                        yield from self.lex_attrs_till()
+                        self.expect("?")
+                        self.expect(">")
+                    elif self.match("!"):
+                        self.expect("-")
+                        self.expect("-")
+                        last3 = ""
+                        while True:
+                            last3 = last3[-2:] + self.getch()
+                            if last3 == "-->":
+                                break
+                    else:
+                        tag = self.getnsident()
+                        yield (START_TAG, tag)
+                        yield from self.lex_attrs_till()
+                        if self.match("/"):
+                            yield (END_TAG, tag)
+                        self.expect(">")
+                else:
+                    text = ""
+                    while self.curch() != "<":
+                        text += self.getch()
+                    if text:
+                        yield (TEXT, text)
+            except EOFError:
+                pass
+
+
+def gfind(gen, pred):
+    for i in gen:
+        if pred(i):
+            return i
+
+
+def text_of(gen, tag):
+    # Return text content of a leaf tag
+    def match_tag(t):
+        if t[0] != START_TAG:
+            return False
+        if isinstance(tag, ()):
+            return t[1] == tag
+        return t[1][1] == tag
+
+    gfind(gen, match_tag)
+    # Assumes no attributes
+    t, val = next(gen)
+    assert t == TEXT
+    return val
+
+
+def tokenize(file):
+    return XMLTokenizer(file).tokenize()
diff --git a/mimetypes/metadata.txt b/mimetypes/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/mimetypes/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/mimetypes/mimetypes.py b/mimetypes/mimetypes.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/mimetypes/setup.py b/mimetypes/setup.py
deleted file mode 100644
index 3244bfeb6..000000000
--- a/mimetypes/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-mimetypes',
-      version='0.0.0',
-      description='Dummy mimetypes module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['mimetypes'])
diff --git a/multiprocessing/metadata.txt b/multiprocessing/metadata.txt
deleted file mode 100644
index b2cc2f0d4..000000000
--- a/multiprocessing/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1
-author = Paul Sokolovsky
-depends = os, select, pickle
diff --git a/multiprocessing/setup.py b/multiprocessing/setup.py
deleted file mode 100644
index 428e2fa7e..000000000
--- a/multiprocessing/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-multiprocessing',
-      version='0.1',
-      description='multiprocessing module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['multiprocessing'],
-      install_requires=['micropython-os', 'micropython-select', 'micropython-pickle'])
diff --git a/multiprocessing/test_pipe.py b/multiprocessing/test_pipe.py
deleted file mode 100644
index 84591d96b..000000000
--- a/multiprocessing/test_pipe.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-import os
-from multiprocessing import Process, Pipe, Connection
-
-def f(conn):
-    conn.send([42, None, 'hello'])
-    conn.send([42, 42, 42])
-    conn.close()
-
-if __name__ == '__main__':
-    parent_conn, child_conn = Pipe(False)
-    print(parent_conn, child_conn)
-    p = Process(target=f, args=(child_conn,))
-    # Extension: need to call this for uPy
-    p.register_pipe(parent_conn, child_conn)
-    p.start()
-    print(parent_conn.recv())
-    print(parent_conn.recv())
-    p.join()
diff --git a/multiprocessing/test_pool.py b/multiprocessing/test_pool.py
deleted file mode 100644
index dcb89a761..000000000
--- a/multiprocessing/test_pool.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from multiprocessing import Pool
-
-def f(x):
-    return x*x
-
-pool = Pool(4)
-print(pool.apply(f, (10,)))
diff --git a/multiprocessing/test_pool_async.py b/multiprocessing/test_pool_async.py
deleted file mode 100644
index f489b2ac3..000000000
--- a/multiprocessing/test_pool_async.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import time
-from multiprocessing import Pool
-
-def f(x):
-    return x*x
-
-pool = Pool(4)
-future = pool.apply_async(f, (10,))
-print(future.get())
-
-def f2(x):
-    time.sleep(1)
-    return x + 1
-
-future = pool.apply_async(f2, (10,))
-while not future.ready():
-    print("not ready")
-    time.sleep(0.2)
-
-print(future.get())
-
-
-t = time.time()
-futs = [
-    pool.apply_async(f2, (10,)),
-    pool.apply_async(f2, (11,)),
-    pool.apply_async(f2, (12,)),
-]
-
-while True:
-    #not all(futs):
-    c = 0
-    for f in futs:
-        if not f.ready():
-            c += 1
-    if not c:
-        break
-    print("not ready2")
-    time.sleep(0.2)
-
-print("Run 3 parallel sleep(1)'s in: ", time.time() - t)
diff --git a/multiprocessing/test_process.py b/multiprocessing/test_process.py
deleted file mode 100644
index 16a6a2abd..000000000
--- a/multiprocessing/test_process.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from multiprocessing import Process
-
-def f(name):
-    print('hello', name)
-
-if __name__ == '__main__':
-    p = Process(target=f, args=('bob',))
-    p.start()
-    p.join()
diff --git a/nntplib/metadata.txt b/nntplib/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/nntplib/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/nntplib/nntplib.py b/nntplib/nntplib.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/nntplib/setup.py b/nntplib/setup.py
deleted file mode 100644
index b3da747f4..000000000
--- a/nntplib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-nntplib',
-      version='0.0.0',
-      description='Dummy nntplib module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['nntplib'])
diff --git a/numbers/metadata.txt b/numbers/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/numbers/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/numbers/numbers.py b/numbers/numbers.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/numbers/setup.py b/numbers/setup.py
deleted file mode 100644
index 0c5d7854d..000000000
--- a/numbers/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-numbers',
-      version='0.0.1',
-      description='Dummy numbers module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['numbers'])
diff --git a/operator/metadata.txt b/operator/metadata.txt
deleted file mode 100644
index a9a85a5bf..000000000
--- a/operator/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1
diff --git a/operator/operator.py b/operator/operator.py
deleted file mode 100644
index 7b8b09985..000000000
--- a/operator/operator.py
+++ /dev/null
@@ -1,5 +0,0 @@
-def attrgetter(attr):
-    assert "." not in attr
-    def _attrgetter(obj):
-        return getattr(obj, attr)
-    return _attrgetter
diff --git a/operator/setup.py b/operator/setup.py
deleted file mode 100644
index 9fbec88cc..000000000
--- a/operator/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-operator',
-      version='0.1',
-      description='operator module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['operator'])
diff --git a/operator/test_operator.py b/operator/test_operator.py
deleted file mode 100644
index 877e9d0ce..000000000
--- a/operator/test_operator.py
+++ /dev/null
@@ -1,7 +0,0 @@
-import operator
-
-class A: pass
-a = A()
-a.name = "foo"
-f = operator.attrgetter('name')
-assert f(a) == "foo"
diff --git a/optparse/metadata.txt b/optparse/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/optparse/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/optparse/optparse.py b/optparse/optparse.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/optparse/setup.py b/optparse/setup.py
deleted file mode 100644
index a58f23527..000000000
--- a/optparse/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-optparse',
-      version='0.0.0',
-      description='Dummy optparse module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['optparse'])
diff --git a/os.path/metadata.txt b/os.path/metadata.txt
deleted file mode 100644
index e571a1707..000000000
--- a/os.path/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = micropython-lib
-type = package
-version = 0.1.2
-author = Paul Sokolovsky
diff --git a/os.path/setup.py b/os.path/setup.py
deleted file mode 100644
index ca0749b5f..000000000
--- a/os.path/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-os.path',
-      version='0.1.2',
-      description='os.path module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['os'])
diff --git a/os.path/test_path.py b/os.path/test_path.py
deleted file mode 100644
index 496e151fd..000000000
--- a/os.path/test_path.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import sys
-sys.path[0] = "os"
-from path import *
-
-assert split("") == ("", "")
-assert split("path") == ("", "path")
-assert split("/") == ("/", "")
-assert split("/foo") == ("/", "foo")
-assert split("/foo/") == ("/foo", "")
-assert split("/foo/bar") == ("/foo", "bar")
-
-assert exists("test_path.py")
-assert not exists("test_path.py--")
-
-assert isdir("os")
-assert not isdir("os--")
-assert not isdir("test_path.py")
diff --git a/os/metadata.txt b/os/metadata.txt
deleted file mode 100644
index 38b2b3f94..000000000
--- a/os/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = package
-version = 0.4
-author = Paul Sokolovsky
-depends = ffilib, errno, stat
diff --git a/os/os/__init__.py b/os/os/__init__.py
deleted file mode 100644
index dec926775..000000000
--- a/os/os/__init__.py
+++ /dev/null
@@ -1,254 +0,0 @@
-import array
-import ustruct as struct
-import errno as errno_
-import stat as stat_
-import ffilib
-import uos
-
-R_OK = const(4)
-W_OK = const(2)
-X_OK = const(1)
-F_OK = const(0)
-
-O_ACCMODE  = 0o0000003
-O_RDONLY   = 0o0000000
-O_WRONLY   = 0o0000001
-O_RDWR     = 0o0000002
-O_CREAT    = 0o0000100
-O_EXCL     = 0o0000200
-O_NOCTTY   = 0o0000400
-O_TRUNC    = 0o0001000
-O_APPEND   = 0o0002000
-O_NONBLOCK = 0o0004000
-
-error = OSError
-name = "posix"
-sep = "/"
-curdir = "."
-pardir = ".."
-environ = {"WARNING": "NOT_IMPLEMENTED"}
-
-
-libc = ffilib.libc()
-
-if libc:
-    chdir_ = libc.func("i", "chdir", "s")
-    mkdir_ = libc.func("i", "mkdir", "si")
-    rename_ = libc.func("i", "rename", "ss")
-    unlink_ = libc.func("i", "unlink", "s")
-    rmdir_ = libc.func("i", "rmdir", "s")
-    getcwd_ = libc.func("s", "getcwd", "si")
-    opendir_ = libc.func("P", "opendir", "s")
-    readdir_ = libc.func("P", "readdir", "P")
-    open_ = libc.func("i", "open", "sii")
-    read_ = libc.func("i", "read", "ipi")
-    write_ = libc.func("i", "write", "iPi")
-    close_ = libc.func("i", "close", "i")
-    dup_ = libc.func("i", "dup", "i")
-    access_ = libc.func("i", "access", "si")
-    fork_ = libc.func("i", "fork", "")
-    pipe_ = libc.func("i", "pipe", "p")
-    _exit_ = libc.func("v", "_exit", "i")
-    getpid_ = libc.func("i", "getpid", "")
-    waitpid_ = libc.func("i", "waitpid", "ipi")
-    system_ = libc.func("i", "system", "s")
-    execvp_ = libc.func("i", "execvp", "PP")
-    kill_ = libc.func("i", "kill", "ii")
-    getenv_ = libc.func("s", "getenv", "P")
-
-
-
-def check_error(ret):
-    # Return True is error was EINTR (which usually means that OS call
-    # should be restarted).
-    if ret == -1:
-        e = uos.errno()
-        if e == errno_.EINTR:
-            return True
-        raise OSError(e)
-
-def raise_error():
-    raise OSError(uos.errno())
-
-
-def getcwd():
-    buf = bytearray(512)
-    return getcwd_(buf, 512)
-
-def mkdir(name, mode=0o777):
-    e = mkdir_(name, mode)
-    check_error(e)
-
-def rename(old, new):
-    e = rename_(old, new)
-    check_error(e)
-
-def unlink(name):
-    e = unlink_(name)
-    check_error(e)
-
-def rmdir(name):
-    e = rmdir_(name)
-    check_error(e)
-
-def makedirs(name, mode=0o777, exist_ok=False):
-    s = ""
-    comps = name.split("/")
-    if comps[-1] == "":
-        comps.pop()
-    for i, c in enumerate(comps):
-        s += c + "/"
-        try:
-            uos.mkdir(s)
-        except OSError as e:
-            if e.args[0] != errno_.EEXIST:
-                raise
-            if i == len(comps) - 1:
-                if exist_ok:
-                    return
-                raise e
-
-if hasattr(uos, "ilistdir"):
-    ilistdir = uos.ilistdir
-else:
-    def ilistdir(path="."):
-        dir = opendir_(path)
-        if not dir:
-            raise_error()
-        res = []
-        dirent_fmt = "LLHB256s"
-        while True:
-            dirent = readdir_(dir)
-            if not dirent:
-                break
-            import uctypes
-            dirent = uctypes.bytes_at(dirent, struct.calcsize(dirent_fmt))
-            dirent = struct.unpack(dirent_fmt, dirent)
-            dirent = (dirent[-1].split(b'\0', 1)[0], dirent[-2], dirent[0])
-            yield dirent
-
-def listdir(path="."):
-    is_str = type(path) is not bytes
-    res = []
-    for dirent in ilistdir(path):
-        fname = dirent[0]
-        if fname not in (b".", b"..", ".", ".."):
-            if is_str:
-                fname = fsdecode(fname)
-            res.append(fname)
-    return res
-
-def walk(top, topdown=True):
-    files = []
-    dirs = []
-    for dirent in ilistdir_ex(top):
-        mode = dirent[3] << 12
-        fname = dirent[4].split(b'\0', 1)[0]
-        if stat_.S_ISDIR(mode):
-            if fname != b"." and fname != b"..":
-                dirs.append(fsdecode(fname))
-        else:
-            files.append(fsdecode(fname))
-    if topdown:
-        yield top, dirs, files
-    for d in dirs:
-        yield from walk(top + "/" + d, topdown)
-    if not topdown:
-        yield top, dirs, files
-
-def open(n, flags, mode=0o777):
-    r = open_(n, flags, mode)
-    check_error(r)
-    return r
-
-def read(fd, n):
-    buf = bytearray(n)
-    r = read_(fd, buf, n)
-    check_error(r)
-    return bytes(buf[:r])
-
-def write(fd, buf):
-    r = write_(fd, buf, len(buf))
-    check_error(r)
-    return r
-
-def close(fd):
-    r = close_(fd)
-    check_error(r)
-    return r
-
-def dup(fd):
-    r = dup_(fd)
-    check_error(r)
-    return r
-
-def access(path, mode):
-    return access_(path, mode) == 0
-
-def chdir(dir):
-    r = chdir_(dir)
-    check_error(r)
-
-def fork():
-    r = fork_()
-    check_error(r)
-    return r
-
-def pipe():
-    a = array.array('i', [0, 0])
-    r = pipe_(a)
-    check_error(r)
-    return a[0], a[1]
-
-def _exit(n):
-    _exit_(n)
-
-def execvp(f, args):
-    import uctypes
-    args_ = array.array("P", [0] * (len(args) + 1))
-    i = 0
-    for a in args:
-        args_[i] = uctypes.addressof(a)
-        i += 1
-    r = execvp_(f, uctypes.addressof(args_))
-    check_error(r)
-
-def getpid():
-    return getpid_()
-
-def waitpid(pid, opts):
-    a = array.array('i', [0])
-    r = waitpid_(pid, a, opts)
-    check_error(r)
-    return (r, a[0])
-
-def kill(pid, sig):
-    r = kill_(pid, sig)
-    check_error(r)
-
-def system(command):
-    r = system_(command)
-    check_error(r)
-    return r
-
-def getenv(var, default=None):
-    var = getenv_(var)
-    if var is None:
-        return default
-    return var
-
-def fsencode(s):
-    if type(s) is bytes:
-        return s
-    return bytes(s, "utf-8")
-
-def fsdecode(s):
-    if type(s) is str:
-        return s
-    return str(s, "utf-8")
-
-
-def urandom(n):
-    import builtins
-    with builtins.open("/dev/urandom", "rb") as f:
-        return f.read(n)
diff --git a/os/setup.py b/os/setup.py
deleted file mode 100644
index 985e83f2c..000000000
--- a/os/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-os',
-      version='0.4',
-      description='os module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['os'],
-      install_requires=['micropython-ffilib', 'micropython-errno', 'micropython-stat'])
diff --git a/os/test_filestat.py b/os/test_filestat.py
deleted file mode 100644
index 91014a929..000000000
--- a/os/test_filestat.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import os
-
-
-assert os.access("test_filestat.py", os.F_OK) == True
-assert os.access("test_filestat.py-not", os.F_OK) == False
diff --git a/pathlib/metadata.txt b/pathlib/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/pathlib/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/pathlib/pathlib.py b/pathlib/pathlib.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/pathlib/setup.py b/pathlib/setup.py
deleted file mode 100644
index c3a5ea953..000000000
--- a/pathlib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-pathlib',
-      version='0.0.0',
-      description='Dummy pathlib module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['pathlib'])
diff --git a/pdb/metadata.txt b/pdb/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/pdb/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/pdb/pdb.py b/pdb/pdb.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/pdb/setup.py b/pdb/setup.py
deleted file mode 100644
index f9f181130..000000000
--- a/pdb/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-pdb',
-      version='0.0.1',
-      description='Dummy pdb module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['pdb'])
diff --git a/pickle/metadata.txt b/pickle/metadata.txt
deleted file mode 100644
index 0cf2a7aba..000000000
--- a/pickle/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.2
diff --git a/pickle/pickle.py b/pickle/pickle.py
deleted file mode 100644
index e455620b7..000000000
--- a/pickle/pickle.py
+++ /dev/null
@@ -1,14 +0,0 @@
-def dump(obj, f):
-    f.write(repr(obj))
-
-def dumps(obj):
-    return repr(obj)
-
-def load(f):
-    s = f.read()
-    return loads(s)
-
-def loads(s):
-    d = {}
-    exec("v=" + s, d)
-    return d["v"]
diff --git a/pickle/setup.py b/pickle/setup.py
deleted file mode 100644
index 14fee4f85..000000000
--- a/pickle/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-pickle',
-      version='0.0.2',
-      description='Dummy pickle module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['pickle'])
diff --git a/pickle/test_pickle.py b/pickle/test_pickle.py
deleted file mode 100644
index 1422c02e9..000000000
--- a/pickle/test_pickle.py
+++ /dev/null
@@ -1,7 +0,0 @@
-import pickle
-import sys
-import io
-
-pickle.dump({1:2}, sys.stdout)
-
-print(pickle.loads("{4:5}"))
diff --git a/pickletools/metadata.txt b/pickletools/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/pickletools/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/pickletools/pickletools.py b/pickletools/pickletools.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/pickletools/setup.py b/pickletools/setup.py
deleted file mode 100644
index 131a921e8..000000000
--- a/pickletools/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-pickletools',
-      version='0.0.0',
-      description='Dummy pickletools module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['pickletools'])
diff --git a/pkg_resources/metadata.txt b/pkg_resources/metadata.txt
deleted file mode 100644
index 6b5dc7328..000000000
--- a/pkg_resources/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=micropython-lib
-type=module
-version=0.1
diff --git a/pkg_resources/pkg_resources.py b/pkg_resources/pkg_resources.py
deleted file mode 100644
index b7488f04b..000000000
--- a/pkg_resources/pkg_resources.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import os
-import os.path
-
-def resource_stream(package, resource):
-    p = __import__(package)
-    d = os.path.dirname(p.__file__)
-    if d[0] != "/":
-        d = os.getcwd() + "/" + d
-    return open(d + "/" + resource, "rb")
diff --git a/pkg_resources/setup.py b/pkg_resources/setup.py
deleted file mode 100644
index d561e3deb..000000000
--- a/pkg_resources/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-pkg_resources',
-      version='0.1',
-      description='pkg_resources module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['pkg_resources'])
diff --git a/pkgutil/metadata.txt b/pkgutil/metadata.txt
deleted file mode 100644
index 6e4f13022..000000000
--- a/pkgutil/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1
-depends = pkg_resources
diff --git a/pkgutil/setup.py b/pkgutil/setup.py
deleted file mode 100644
index 417a1c390..000000000
--- a/pkgutil/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-pkgutil',
-      version='0.1',
-      description='pkgutil module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['pkgutil'],
-      install_requires=['micropython-pkg_resources'])
diff --git a/platform/metadata.txt b/platform/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/platform/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/platform/platform.py b/platform/platform.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/platform/setup.py b/platform/setup.py
deleted file mode 100644
index 7e750c1e3..000000000
--- a/platform/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-platform',
-      version='0.0.1',
-      description='Dummy platform module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['platform'])
diff --git a/poplib/metadata.txt b/poplib/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/poplib/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/poplib/poplib.py b/poplib/poplib.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/poplib/setup.py b/poplib/setup.py
deleted file mode 100644
index d111e3f04..000000000
--- a/poplib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-poplib',
-      version='0.0.0',
-      description='Dummy poplib module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['poplib'])
diff --git a/posixpath/metadata.txt b/posixpath/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/posixpath/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/posixpath/posixpath.py b/posixpath/posixpath.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/posixpath/setup.py b/posixpath/setup.py
deleted file mode 100644
index fc9fd5585..000000000
--- a/posixpath/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-posixpath',
-      version='0.0.0',
-      description='Dummy posixpath module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['posixpath'])
diff --git a/pprint/metadata.txt b/pprint/metadata.txt
deleted file mode 100644
index 0cf2a7aba..000000000
--- a/pprint/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.2
diff --git a/pprint/pprint.py b/pprint/pprint.py
deleted file mode 100644
index 487b61a3d..000000000
--- a/pprint/pprint.py
+++ /dev/null
@@ -1,2 +0,0 @@
-def pformat(obj):
-    return repr(obj)
diff --git a/pprint/setup.py b/pprint/setup.py
deleted file mode 100644
index b3329ce4a..000000000
--- a/pprint/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-pprint',
-      version='0.0.2',
-      description='Dummy pprint module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['pprint'])
diff --git a/profile/metadata.txt b/profile/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/profile/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/profile/profile.py b/profile/profile.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/profile/setup.py b/profile/setup.py
deleted file mode 100644
index a3b7b9acf..000000000
--- a/profile/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-profile',
-      version='0.0.0',
-      description='Dummy profile module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['profile'])
diff --git a/pty/metadata.txt b/pty/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/pty/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/pty/pty.py b/pty/pty.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/pty/setup.py b/pty/setup.py
deleted file mode 100644
index dbdc916a6..000000000
--- a/pty/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-pty',
-      version='0.0.0',
-      description='Dummy pty module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['pty'])
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..83d29405d
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,101 @@
+[tool.ruff]
+exclude = [
+  "python-stdlib",
+  "unix-ffi",
+]
+select = [
+  "ASYNC",  # flake8-comprehensions
+  "C4",     # flake8-comprehensions
+  "C90",    # McCabe cyclomatic complexity
+  "DTZ",    # flake8-datetimez
+  "E",      # pycodestyle
+  "EXE",    # flake8-executable
+  "F",      # Pyflakes
+  "G",      # flake8-logging-format
+  "ICN",    # flake8-import-conventions
+  "INT",    # flake8-gettext
+  "ISC",    # flake8-implicit-str-concat
+  "PGH",    # pygrep-hooks
+  "PIE",    # flake8-pie
+  "PL",     # Pylint
+  "PYI",    # flake8-pyi
+  "RSE",    # flake8-raise
+  "RUF",    # Ruff-specific rules
+  "T10",    # flake8-debugger
+  "TCH",    # flake8-type-checking
+  "W",      # pycodestyle
+  "YTT",    # flake8-2020
+  # "A",    # flake8-builtins
+  # "ANN",  # flake8-annotations
+  # "ARG",  # flake8-unused-arguments
+  # "B",    # flake8-bugbear
+  # "BLE",  # flake8-blind-except
+  # "COM",  # flake8-commas
+  # "D",    # pydocstyle
+  # "DJ",   # flake8-django
+  # "EM",   # flake8-errmsg
+  # "ERA",  # eradicate
+  # "FBT",  # flake8-boolean-trap
+  # "I",    # isort
+  # "INP",  # flake8-no-pep420
+  # "N",    # pep8-naming
+  # "NPY",  # NumPy-specific rules
+  # "PD",   # pandas-vet
+  # "PT",   # flake8-pytest-style
+  # "PTH",  # flake8-use-pathlib
+  # "Q",    # flake8-quotes
+  # "RET",  # flake8-return
+  # "S",    # flake8-bandit
+  # "SIM",  # flake8-simplify
+  # "SLF",  # flake8-self
+  # "T20",  # flake8-print
+  # "TID",  # flake8-tidy-imports
+  # "TRY",  # tryceratops
+  # "UP",   # pyupgrade
+]
+ignore = [
+  "E722",
+  "E741",  # 'l' is currently widely used
+  "F401",
+  "F403",
+  "F405",
+  "E501",  # line length, recommended to disable
+  "ISC001",
+  "ISC003",  # micropython does not support implicit concatenation of f-strings
+  "PIE810",  # micropython does not support passing tuples to .startswith or .endswith
+  "PLC1901",
+  "PLR1704",  # sometimes desirable to redefine an argument to save code size
+  "PLR1714",
+  "PLR5501",
+  "PLW0602",
+  "PLW0603",
+  "PLW2901",
+  "RUF012",
+  "RUF100",
+  "SIM101",
+  "W191",  # tab-indent, redundant when using formatter
+]
+line-length = 99
+target-version = "py37"
+
+[tool.ruff.mccabe]
+max-complexity = 61
+
+[tool.ruff.pylint]
+allow-magic-value-types = ["bytes", "int", "str"]
+max-args = 14
+max-branches = 58
+max-returns = 13
+max-statements = 166
+
+[tool.ruff.per-file-ignores]
+"micropython/aiorepl/aiorepl.py" = ["PGH001"]
+
+# manifest.py files are evaluated with some global names pre-defined
+"**/manifest.py" = ["F821"]
+"ports/**/boards/manifest*.py" = ["F821"]
+
+# ble multitests are evaluated with some names pre-defined
+"micropython/bluetooth/aioble/multitests/*" = ["F821"]
+
+[tool.ruff.format]
diff --git a/pystone/metadata.txt b/pystone/metadata.txt
deleted file mode 100644
index fc82994ce..000000000
--- a/pystone/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = cpython
-type = module
-version = 3.4.2-1
diff --git a/pystone/pystone.py b/pystone/pystone.py
deleted file mode 100755
index 2280c536a..000000000
--- a/pystone/pystone.py
+++ /dev/null
@@ -1,277 +0,0 @@
-#! /usr/bin/env python3
-
-"""
-"PYSTONE" Benchmark Program
-
-Version:        Python/1.2 (corresponds to C/1.1 plus 3 Pystone fixes)
-
-Author:         Reinhold P. Weicker,  CACM Vol 27, No 10, 10/84 pg. 1013.
-
-                Translated from ADA to C by Rick Richardson.
-                Every method to preserve ADA-likeness has been used,
-                at the expense of C-ness.
-
-                Translated from C to Python by Guido van Rossum.
-
-Version History:
-
-                Version 1.1 corrects two bugs in version 1.0:
-
-                First, it leaked memory: in Proc1(), NextRecord ends
-                up having a pointer to itself.  I have corrected this
-                by zapping NextRecord.PtrComp at the end of Proc1().
-
-                Second, Proc3() used the operator != to compare a
-                record to None.  This is rather inefficient and not
-                true to the intention of the original benchmark (where
-                a pointer comparison to None is intended; the !=
-                operator attempts to find a method __cmp__ to do value
-                comparison of the record).  Version 1.1 runs 5-10
-                percent faster than version 1.0, so benchmark figures
-                of different versions can't be compared directly.
-
-                Version 1.2 changes the division to floor division.
-
-                Under Python 3 version 1.1 would use the normal division
-                operator, resulting in some of the operations mistakenly
-                yielding floats. Version 1.2 instead uses floor division
-                making the benchmark a integer benchmark again.
-
-"""
-
-LOOPS = 50000
-
-from utime import clock
-
-__version__ = "1.2"
-
-[Ident1, Ident2, Ident3, Ident4, Ident5] = range(1, 6)
-
-class Record:
-
-    def __init__(self, PtrComp = None, Discr = 0, EnumComp = 0,
-                       IntComp = 0, StringComp = 0):
-        self.PtrComp = PtrComp
-        self.Discr = Discr
-        self.EnumComp = EnumComp
-        self.IntComp = IntComp
-        self.StringComp = StringComp
-
-    def copy(self):
-        return Record(self.PtrComp, self.Discr, self.EnumComp,
-                      self.IntComp, self.StringComp)
-
-TRUE = 1
-FALSE = 0
-
-def main(loops=LOOPS):
-    benchtime, stones = pystones(loops)
-    print("Pystone(%s) time for %d passes = %g" % \
-          (__version__, loops, benchtime))
-    print("This machine benchmarks at %g pystones/second" % stones)
-
-
-def pystones(loops=LOOPS):
-    return Proc0(loops)
-
-IntGlob = 0
-BoolGlob = FALSE
-Char1Glob = '\0'
-Char2Glob = '\0'
-Array1Glob = [0]*51
-Array2Glob = [x[:] for x in [Array1Glob]*51]
-PtrGlb = None
-PtrGlbNext = None
-
-def Proc0(loops=LOOPS):
-    global IntGlob
-    global BoolGlob
-    global Char1Glob
-    global Char2Glob
-    global Array1Glob
-    global Array2Glob
-    global PtrGlb
-    global PtrGlbNext
-
-    starttime = clock()
-    for i in range(loops):
-        pass
-    nulltime = clock() - starttime
-
-    PtrGlbNext = Record()
-    PtrGlb = Record()
-    PtrGlb.PtrComp = PtrGlbNext
-    PtrGlb.Discr = Ident1
-    PtrGlb.EnumComp = Ident3
-    PtrGlb.IntComp = 40
-    PtrGlb.StringComp = "DHRYSTONE PROGRAM, SOME STRING"
-    String1Loc = "DHRYSTONE PROGRAM, 1'ST STRING"
-    Array2Glob[8][7] = 10
-
-    starttime = clock()
-
-    for i in range(loops):
-        Proc5()
-        Proc4()
-        IntLoc1 = 2
-        IntLoc2 = 3
-        String2Loc = "DHRYSTONE PROGRAM, 2'ND STRING"
-        EnumLoc = Ident2
-        BoolGlob = not Func2(String1Loc, String2Loc)
-        while IntLoc1 < IntLoc2:
-            IntLoc3 = 5 * IntLoc1 - IntLoc2
-            IntLoc3 = Proc7(IntLoc1, IntLoc2)
-            IntLoc1 = IntLoc1 + 1
-        Proc8(Array1Glob, Array2Glob, IntLoc1, IntLoc3)
-        PtrGlb = Proc1(PtrGlb)
-        CharIndex = 'A'
-        while CharIndex <= Char2Glob:
-            if EnumLoc == Func1(CharIndex, 'C'):
-                EnumLoc = Proc6(Ident1)
-            CharIndex = chr(ord(CharIndex)+1)
-        IntLoc3 = IntLoc2 * IntLoc1
-        IntLoc2 = IntLoc3 // IntLoc1
-        IntLoc2 = 7 * (IntLoc3 - IntLoc2) - IntLoc1
-        IntLoc1 = Proc2(IntLoc1)
-
-    benchtime = clock() - starttime - nulltime
-    if benchtime == 0.0:
-        loopsPerBenchtime = 0.0
-    else:
-        loopsPerBenchtime = (loops / benchtime)
-    return benchtime, loopsPerBenchtime
-
-def Proc1(PtrParIn):
-    PtrParIn.PtrComp = NextRecord = PtrGlb.copy()
-    PtrParIn.IntComp = 5
-    NextRecord.IntComp = PtrParIn.IntComp
-    NextRecord.PtrComp = PtrParIn.PtrComp
-    NextRecord.PtrComp = Proc3(NextRecord.PtrComp)
-    if NextRecord.Discr == Ident1:
-        NextRecord.IntComp = 6
-        NextRecord.EnumComp = Proc6(PtrParIn.EnumComp)
-        NextRecord.PtrComp = PtrGlb.PtrComp
-        NextRecord.IntComp = Proc7(NextRecord.IntComp, 10)
-    else:
-        PtrParIn = NextRecord.copy()
-    NextRecord.PtrComp = None
-    return PtrParIn
-
-def Proc2(IntParIO):
-    IntLoc = IntParIO + 10
-    while 1:
-        if Char1Glob == 'A':
-            IntLoc = IntLoc - 1
-            IntParIO = IntLoc - IntGlob
-            EnumLoc = Ident1
-        if EnumLoc == Ident1:
-            break
-    return IntParIO
-
-def Proc3(PtrParOut):
-    global IntGlob
-
-    if PtrGlb is not None:
-        PtrParOut = PtrGlb.PtrComp
-    else:
-        IntGlob = 100
-    PtrGlb.IntComp = Proc7(10, IntGlob)
-    return PtrParOut
-
-def Proc4():
-    global Char2Glob
-
-    BoolLoc = Char1Glob == 'A'
-    BoolLoc = BoolLoc or BoolGlob
-    Char2Glob = 'B'
-
-def Proc5():
-    global Char1Glob
-    global BoolGlob
-
-    Char1Glob = 'A'
-    BoolGlob = FALSE
-
-def Proc6(EnumParIn):
-    EnumParOut = EnumParIn
-    if not Func3(EnumParIn):
-        EnumParOut = Ident4
-    if EnumParIn == Ident1:
-        EnumParOut = Ident1
-    elif EnumParIn == Ident2:
-        if IntGlob > 100:
-            EnumParOut = Ident1
-        else:
-            EnumParOut = Ident4
-    elif EnumParIn == Ident3:
-        EnumParOut = Ident2
-    elif EnumParIn == Ident4:
-        pass
-    elif EnumParIn == Ident5:
-        EnumParOut = Ident3
-    return EnumParOut
-
-def Proc7(IntParI1, IntParI2):
-    IntLoc = IntParI1 + 2
-    IntParOut = IntParI2 + IntLoc
-    return IntParOut
-
-def Proc8(Array1Par, Array2Par, IntParI1, IntParI2):
-    global IntGlob
-
-    IntLoc = IntParI1 + 5
-    Array1Par[IntLoc] = IntParI2
-    Array1Par[IntLoc+1] = Array1Par[IntLoc]
-    Array1Par[IntLoc+30] = IntLoc
-    for IntIndex in range(IntLoc, IntLoc+2):
-        Array2Par[IntLoc][IntIndex] = IntLoc
-    Array2Par[IntLoc][IntLoc-1] = Array2Par[IntLoc][IntLoc-1] + 1
-    Array2Par[IntLoc+20][IntLoc] = Array1Par[IntLoc]
-    IntGlob = 5
-
-def Func1(CharPar1, CharPar2):
-    CharLoc1 = CharPar1
-    CharLoc2 = CharLoc1
-    if CharLoc2 != CharPar2:
-        return Ident1
-    else:
-        return Ident2
-
-def Func2(StrParI1, StrParI2):
-    IntLoc = 1
-    while IntLoc <= 1:
-        if Func1(StrParI1[IntLoc], StrParI2[IntLoc+1]) == Ident1:
-            CharLoc = 'A'
-            IntLoc = IntLoc + 1
-    if CharLoc >= 'W' and CharLoc <= 'Z':
-        IntLoc = 7
-    if CharLoc == 'X':
-        return TRUE
-    else:
-        if StrParI1 > StrParI2:
-            IntLoc = IntLoc + 7
-            return TRUE
-        else:
-            return FALSE
-
-def Func3(EnumParIn):
-    EnumLoc = EnumParIn
-    if EnumLoc == Ident3: return TRUE
-    return FALSE
-
-if __name__ == '__main__':
-    import sys
-    def error(msg):
-        print(msg, end=' ', file=sys.stderr)
-        print("usage: %s [number_of_loops]" % sys.argv[0], file=sys.stderr)
-        sys.exit(100)
-    nargs = len(sys.argv) - 1
-    if nargs > 1:
-        error("%d arguments are too many;" % nargs)
-    elif nargs == 1:
-        try: loops = int(sys.argv[1])
-        except ValueError:
-            error("Invalid argument %r;" % sys.argv[1])
-    else:
-        loops = LOOPS
-    main(loops)
diff --git a/pystone/setup.py b/pystone/setup.py
deleted file mode 100644
index 11f57a4ba..000000000
--- a/pystone/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-pystone',
-      version='3.4.2-1',
-      description='CPython pystone module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['pystone'])
diff --git a/pystone_lowmem/metadata.txt b/pystone_lowmem/metadata.txt
deleted file mode 100644
index fc82994ce..000000000
--- a/pystone_lowmem/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = cpython
-type = module
-version = 3.4.2-1
diff --git a/pystone_lowmem/pystone_lowmem.py b/pystone_lowmem/pystone_lowmem.py
deleted file mode 100755
index 8bcf4e156..000000000
--- a/pystone_lowmem/pystone_lowmem.py
+++ /dev/null
@@ -1,277 +0,0 @@
-#! /usr/bin/env python3
-
-"""
-"PYSTONE" Benchmark Program
-
-Version:        Python/1.2 (corresponds to C/1.1 plus 3 Pystone fixes)
-
-Author:         Reinhold P. Weicker,  CACM Vol 27, No 10, 10/84 pg. 1013.
-
-                Translated from ADA to C by Rick Richardson.
-                Every method to preserve ADA-likeness has been used,
-                at the expense of C-ness.
-
-                Translated from C to Python by Guido van Rossum.
-
-Version History:
-
-                Version 1.1 corrects two bugs in version 1.0:
-
-                First, it leaked memory: in Proc1(), NextRecord ends
-                up having a pointer to itself.  I have corrected this
-                by zapping NextRecord.PtrComp at the end of Proc1().
-
-                Second, Proc3() used the operator != to compare a
-                record to None.  This is rather inefficient and not
-                true to the intention of the original benchmark (where
-                a pointer comparison to None is intended; the !=
-                operator attempts to find a method __cmp__ to do value
-                comparison of the record).  Version 1.1 runs 5-10
-                percent faster than version 1.0, so benchmark figures
-                of different versions can't be compared directly.
-
-                Version 1.2 changes the division to floor division.
-
-                Under Python 3 version 1.1 would use the normal division
-                operator, resulting in some of the operations mistakenly
-                yielding floats. Version 1.2 instead uses floor division
-                making the benchmark a integer benchmark again.
-
-"""
-
-LOOPS = 50000
-
-from utime import ticks_ms, ticks_diff
-
-__version__ = "1.2"
-
-[Ident1, Ident2, Ident3, Ident4, Ident5] = range(1, 6)
-
-class Record:
-
-    def __init__(self, PtrComp = None, Discr = 0, EnumComp = 0,
-                       IntComp = 0, StringComp = 0):
-        self.PtrComp = PtrComp
-        self.Discr = Discr
-        self.EnumComp = EnumComp
-        self.IntComp = IntComp
-        self.StringComp = StringComp
-
-    def copy(self):
-        return Record(self.PtrComp, self.Discr, self.EnumComp,
-                      self.IntComp, self.StringComp)
-
-TRUE = 1
-FALSE = 0
-
-def main(loops=LOOPS):
-    benchtime, stones = pystones(loops)
-    print("Pystone(%s) time for %d passes = %g" % \
-          (__version__, loops, benchtime))
-    print("This machine benchmarks at %g pystones/second" % stones)
-
-
-def pystones(loops=LOOPS):
-    return Proc0(loops)
-
-IntGlob = 0
-BoolGlob = FALSE
-Char1Glob = '\0'
-Char2Glob = '\0'
-Array1Glob = [0]*(51 // 2)
-Array2Glob = [x[:] for x in [Array1Glob]*(51 // 2)]
-PtrGlb = None
-PtrGlbNext = None
-
-def Proc0(loops=LOOPS):
-    global IntGlob
-    global BoolGlob
-    global Char1Glob
-    global Char2Glob
-    global Array1Glob
-    global Array2Glob
-    global PtrGlb
-    global PtrGlbNext
-
-    starttime = ticks_ms()
-    for i in range(loops):
-        pass
-    nulltime = ticks_diff(starttime, ticks_ms())
-
-    PtrGlbNext = Record()
-    PtrGlb = Record()
-    PtrGlb.PtrComp = PtrGlbNext
-    PtrGlb.Discr = Ident1
-    PtrGlb.EnumComp = Ident3
-    PtrGlb.IntComp = 40
-    PtrGlb.StringComp = "DHRYSTONE PROGRAM, SOME STRING"
-    String1Loc = "DHRYSTONE PROGRAM, 1'ST STRING"
-    Array2Glob[8 // 2][7 // 2] = 10
-
-    starttime = ticks_ms()
-
-    for i in range(loops):
-        Proc5()
-        Proc4()
-        IntLoc1 = 2
-        IntLoc2 = 3
-        String2Loc = "DHRYSTONE PROGRAM, 2'ND STRING"
-        EnumLoc = Ident2
-        BoolGlob = not Func2(String1Loc, String2Loc)
-        while IntLoc1 < IntLoc2:
-            IntLoc3 = 5 * IntLoc1 - IntLoc2
-            IntLoc3 = Proc7(IntLoc1, IntLoc2)
-            IntLoc1 = IntLoc1 + 1
-        Proc8(Array1Glob, Array2Glob, IntLoc1, IntLoc3)
-        PtrGlb = Proc1(PtrGlb)
-        CharIndex = 'A'
-        while CharIndex <= Char2Glob:
-            if EnumLoc == Func1(CharIndex, 'C'):
-                EnumLoc = Proc6(Ident1)
-            CharIndex = chr(ord(CharIndex)+1)
-        IntLoc3 = IntLoc2 * IntLoc1
-        IntLoc2 = IntLoc3 // IntLoc1
-        IntLoc2 = 7 * (IntLoc3 - IntLoc2) - IntLoc1
-        IntLoc1 = Proc2(IntLoc1)
-
-    benchtime = ticks_diff(starttime, ticks_ms()) - nulltime
-    if benchtime == 0.0:
-        loopsPerBenchtime = 0.0
-    else:
-        loopsPerBenchtime = (loops / benchtime) * 1000
-    return benchtime / 1000, loopsPerBenchtime
-
-def Proc1(PtrParIn):
-    PtrParIn.PtrComp = NextRecord = PtrGlb.copy()
-    PtrParIn.IntComp = 5
-    NextRecord.IntComp = PtrParIn.IntComp
-    NextRecord.PtrComp = PtrParIn.PtrComp
-    NextRecord.PtrComp = Proc3(NextRecord.PtrComp)
-    if NextRecord.Discr == Ident1:
-        NextRecord.IntComp = 6
-        NextRecord.EnumComp = Proc6(PtrParIn.EnumComp)
-        NextRecord.PtrComp = PtrGlb.PtrComp
-        NextRecord.IntComp = Proc7(NextRecord.IntComp, 10)
-    else:
-        PtrParIn = NextRecord.copy()
-    NextRecord.PtrComp = None
-    return PtrParIn
-
-def Proc2(IntParIO):
-    IntLoc = IntParIO + 10
-    while 1:
-        if Char1Glob == 'A':
-            IntLoc = IntLoc - 1
-            IntParIO = IntLoc - IntGlob
-            EnumLoc = Ident1
-        if EnumLoc == Ident1:
-            break
-    return IntParIO
-
-def Proc3(PtrParOut):
-    global IntGlob
-
-    if PtrGlb is not None:
-        PtrParOut = PtrGlb.PtrComp
-    else:
-        IntGlob = 100
-    PtrGlb.IntComp = Proc7(10, IntGlob)
-    return PtrParOut
-
-def Proc4():
-    global Char2Glob
-
-    BoolLoc = Char1Glob == 'A'
-    BoolLoc = BoolLoc or BoolGlob
-    Char2Glob = 'B'
-
-def Proc5():
-    global Char1Glob
-    global BoolGlob
-
-    Char1Glob = 'A'
-    BoolGlob = FALSE
-
-def Proc6(EnumParIn):
-    EnumParOut = EnumParIn
-    if not Func3(EnumParIn):
-        EnumParOut = Ident4
-    if EnumParIn == Ident1:
-        EnumParOut = Ident1
-    elif EnumParIn == Ident2:
-        if IntGlob > 100:
-            EnumParOut = Ident1
-        else:
-            EnumParOut = Ident4
-    elif EnumParIn == Ident3:
-        EnumParOut = Ident2
-    elif EnumParIn == Ident4:
-        pass
-    elif EnumParIn == Ident5:
-        EnumParOut = Ident3
-    return EnumParOut
-
-def Proc7(IntParI1, IntParI2):
-    IntLoc = IntParI1 + 2
-    IntParOut = IntParI2 + IntLoc
-    return IntParOut
-
-def Proc8(Array1Par, Array2Par, IntParI1, IntParI2):
-    global IntGlob
-
-    IntLoc = IntParI1 + 5
-    Array1Par[IntLoc // 2] = IntParI2
-    Array1Par[(IntLoc+1) // 2] = Array1Par[IntLoc // 2]
-    Array1Par[(IntLoc+30) // 2] = IntLoc
-    for IntIndex in range(IntLoc, IntLoc+2):
-        Array2Par[IntLoc // 2][IntIndex // 2] = IntLoc
-    Array2Par[IntLoc // 2][(IntLoc-1) // 2] = Array2Par[IntLoc // 2][(IntLoc-1) // 2] + 1
-    Array2Par[(IntLoc+20) // 2][IntLoc // 2] = Array1Par[IntLoc // 2]
-    IntGlob = 5
-
-def Func1(CharPar1, CharPar2):
-    CharLoc1 = CharPar1
-    CharLoc2 = CharLoc1
-    if CharLoc2 != CharPar2:
-        return Ident1
-    else:
-        return Ident2
-
-def Func2(StrParI1, StrParI2):
-    IntLoc = 1
-    while IntLoc <= 1:
-        if Func1(StrParI1[IntLoc], StrParI2[IntLoc+1]) == Ident1:
-            CharLoc = 'A'
-            IntLoc = IntLoc + 1
-    if CharLoc >= 'W' and CharLoc <= 'Z':
-        IntLoc = 7
-    if CharLoc == 'X':
-        return TRUE
-    else:
-        if StrParI1 > StrParI2:
-            IntLoc = IntLoc + 7
-            return TRUE
-        else:
-            return FALSE
-
-def Func3(EnumParIn):
-    EnumLoc = EnumParIn
-    if EnumLoc == Ident3: return TRUE
-    return FALSE
-
-if __name__ == '__main__':
-    import sys
-    def error(msg):
-        print(msg, end=' ', file=sys.stderr)
-        print("usage: %s [number_of_loops]" % sys.argv[0], file=sys.stderr)
-        sys.exit(100)
-    nargs = len(sys.argv) - 1
-    if nargs > 1:
-        error("%d arguments are too many;" % nargs)
-    elif nargs == 1:
-        try: loops = int(sys.argv[1])
-        except ValueError:
-            error("Invalid argument %r;" % sys.argv[1])
-    else:
-        loops = LOOPS
-    main(loops)
diff --git a/pystone_lowmem/setup.py b/pystone_lowmem/setup.py
deleted file mode 100644
index 112ad82f5..000000000
--- a/pystone_lowmem/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-pystone_lowmem',
-      version='3.4.2-1',
-      description='CPython pystone_lowmem module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['pystone_lowmem'])
diff --git a/python-ecosys/README.md b/python-ecosys/README.md
new file mode 100644
index 000000000..9ba6d720d
--- /dev/null
+++ b/python-ecosys/README.md
@@ -0,0 +1,10 @@
+## Python-ecosystem packages
+
+These MicroPython versions of common Python packages, typically found on PyPI.
+
+If a package has the same name as a PyPI package, then it should match at
+least some subset of the functionality.
+
+### Future plans
+
+* More organised directory structure based on library purpose (e.g. drivers, network, etc).
diff --git a/python-ecosys/aiohttp/README.md b/python-ecosys/aiohttp/README.md
new file mode 100644
index 000000000..5ce5e14bc
--- /dev/null
+++ b/python-ecosys/aiohttp/README.md
@@ -0,0 +1,32 @@
+aiohttp is an HTTP client module for MicroPython asyncio module,
+with API mostly compatible with CPython [aiohttp](https://github.com/aio-libs/aiohttp)
+module.
+
+> [!NOTE]
+> Only client is implemented.
+
+See `examples/client.py`
+```py
+import aiohttp
+import asyncio
+
+async def main():
+
+    async with aiohttp.ClientSession() as session:
+        async with session.get('http://micropython.org') as response:
+
+            print("Status:", response.status)
+            print("Content-Type:", response.headers['Content-Type'])
+
+            html = await response.text()
+            print("Body:", html[:15], "...")
+
+asyncio.run(main())
+```
+```
+$ micropython examples/client.py
+Status: 200
+Content-Type: text/html; charset=utf-8
+Body: <!DOCTYPE html> ...
+
+```
diff --git a/python-ecosys/aiohttp/aiohttp/__init__.py b/python-ecosys/aiohttp/aiohttp/__init__.py
new file mode 100644
index 000000000..1e6d89d05
--- /dev/null
+++ b/python-ecosys/aiohttp/aiohttp/__init__.py
@@ -0,0 +1,277 @@
+# MicroPython aiohttp library
+# MIT license; Copyright (c) 2023 Carlos Gil
+
+import asyncio
+import json as _json
+from .aiohttp_ws import (
+    _WSRequestContextManager,
+    ClientWebSocketResponse,
+    WebSocketClient,
+    WSMsgType,
+)
+
+HttpVersion10 = "HTTP/1.0"
+HttpVersion11 = "HTTP/1.1"
+
+
+class ClientResponse:
+    def __init__(self, reader):
+        self.content = reader
+
+    def _get_header(self, keyname, default):
+        for k in self.headers:
+            if k.lower() == keyname:
+                return self.headers[k]
+        return default
+
+    def _decode(self, data):
+        c_encoding = self._get_header("content-encoding", None)
+        if c_encoding in ("gzip", "deflate", "gzip,deflate"):
+            try:
+                import deflate
+                import io
+
+                if c_encoding == "deflate":
+                    with deflate.DeflateIO(io.BytesIO(data), deflate.ZLIB) as d:
+                        return d.read()
+                elif c_encoding == "gzip":
+                    with deflate.DeflateIO(io.BytesIO(data), deflate.GZIP, 15) as d:
+                        return d.read()
+            except ImportError:
+                print("WARNING: deflate module required")
+        return data
+
+    async def read(self, sz=-1):
+        return self._decode(
+            await (self.content.read(sz) if sz == -1 else self.content.readexactly(sz))
+        )
+
+    async def text(self, encoding="utf-8"):
+        return (await self.read(int(self._get_header("content-length", -1)))).decode(encoding)
+
+    async def json(self):
+        return _json.loads(await self.read(int(self._get_header("content-length", -1))))
+
+    def __repr__(self):
+        return "<ClientResponse %d %s>" % (self.status, self.headers)
+
+
+class ChunkedClientResponse(ClientResponse):
+    def __init__(self, reader):
+        self.content = reader
+        self.chunk_size = 0
+
+    async def read(self, sz=4 * 1024 * 1024):
+        if self.chunk_size == 0:
+            l = await self.content.readline()
+            l = l.split(b";", 1)[0]
+            self.chunk_size = int(l, 16)
+            if self.chunk_size == 0:
+                # End of message
+                sep = await self.content.readexactly(2)
+                assert sep == b"\r\n"
+                return b""
+        data = await self.content.readexactly(min(sz, self.chunk_size))
+        self.chunk_size -= len(data)
+        if self.chunk_size == 0:
+            sep = await self.content.readexactly(2)
+            assert sep == b"\r\n"
+        return self._decode(data)
+
+    def __repr__(self):
+        return "<ChunkedClientResponse %d %s>" % (self.status, self.headers)
+
+
+class _RequestContextManager:
+    def __init__(self, client, request_co):
+        self.reqco = request_co
+        self.client = client
+
+    async def __aenter__(self):
+        return await self.reqco
+
+    async def __aexit__(self, *args):
+        await self.client._reader.aclose()
+        return await asyncio.sleep(0)
+
+
+class ClientSession:
+    def __init__(self, base_url="", headers={}, version=HttpVersion10):
+        self._reader = None
+        self._base_url = base_url
+        self._base_headers = {"Connection": "close", "User-Agent": "compat"}
+        self._base_headers.update(**headers)
+        self._http_version = version
+
+    async def __aenter__(self):
+        return self
+
+    async def __aexit__(self, *args):
+        return await asyncio.sleep(0)
+
+    # TODO: Implement timeouts
+
+    async def _request(self, method, url, data=None, json=None, ssl=None, params=None, headers={}):
+        redir_cnt = 0
+        while redir_cnt < 2:
+            reader = await self.request_raw(method, url, data, json, ssl, params, headers)
+            _headers = []
+            sline = await reader.readline()
+            sline = sline.split(None, 2)
+            status = int(sline[1])
+            chunked = False
+            while True:
+                line = await reader.readline()
+                if not line or line == b"\r\n":
+                    break
+                _headers.append(line)
+                if line.startswith(b"Transfer-Encoding:"):
+                    if b"chunked" in line:
+                        chunked = True
+                elif line.startswith(b"Location:"):
+                    url = line.rstrip().split(None, 1)[1].decode()
+
+            if 301 <= status <= 303:
+                redir_cnt += 1
+                await reader.aclose()
+                continue
+            break
+
+        if chunked:
+            resp = ChunkedClientResponse(reader)
+        else:
+            resp = ClientResponse(reader)
+        resp.status = status
+        resp.headers = _headers
+        resp.url = url
+        if params:
+            resp.url += "?" + "&".join(f"{k}={params[k]}" for k in sorted(params))
+        try:
+            resp.headers = {
+                val.split(":", 1)[0]: val.split(":", 1)[-1].strip()
+                for val in [hed.decode().strip() for hed in _headers]
+            }
+        except Exception:
+            pass
+        self._reader = reader
+        return resp
+
+    async def request_raw(
+        self,
+        method,
+        url,
+        data=None,
+        json=None,
+        ssl=None,
+        params=None,
+        headers={},
+        is_handshake=False,
+        version=None,
+    ):
+        if json and isinstance(json, dict):
+            data = _json.dumps(json)
+        if data is not None and method == "GET":
+            method = "POST"
+        if params:
+            url += "?" + "&".join(f"{k}={params[k]}" for k in sorted(params))
+        try:
+            proto, dummy, host, path = url.split("/", 3)
+        except ValueError:
+            proto, dummy, host = url.split("/", 2)
+            path = ""
+
+        if proto == "http:":
+            port = 80
+        elif proto == "https:":
+            port = 443
+            if ssl is None:
+                ssl = True
+        else:
+            raise ValueError("Unsupported protocol: " + proto)
+
+        if ":" in host:
+            host, port = host.split(":", 1)
+            port = int(port)
+
+        reader, writer = await asyncio.open_connection(host, port, ssl=ssl)
+
+        # Use protocol 1.0, because 1.1 always allows to use chunked transfer-encoding
+        # But explicitly set Connection: close, even though this should be default for 1.0,
+        # because some servers misbehave w/o it.
+        if version is None:
+            version = self._http_version
+        if "Host" not in headers:
+            headers.update(Host=host)
+        if not data:
+            query = b"%s /%s %s\r\n%s\r\n" % (
+                method,
+                path,
+                version,
+                "\r\n".join(f"{k}: {v}" for k, v in headers.items()) + "\r\n" if headers else "",
+            )
+        else:
+            if json:
+                headers.update(**{"Content-Type": "application/json"})
+            if isinstance(data, bytes):
+                headers.update(**{"Content-Type": "application/octet-stream"})
+            else:
+                data = data.encode()
+
+            headers.update(**{"Content-Length": len(data)})
+            query = b"""%s /%s %s\r\n%s\r\n%s""" % (
+                method,
+                path,
+                version,
+                "\r\n".join(f"{k}: {v}" for k, v in headers.items()) + "\r\n",
+                data,
+            )
+        if not is_handshake:
+            await writer.awrite(query)
+            return reader
+        else:
+            await writer.awrite(query)
+            return reader, writer
+
+    def request(self, method, url, data=None, json=None, ssl=None, params=None, headers={}):
+        return _RequestContextManager(
+            self,
+            self._request(
+                method,
+                self._base_url + url,
+                data=data,
+                json=json,
+                ssl=ssl,
+                params=params,
+                headers=dict(**self._base_headers, **headers),
+            ),
+        )
+
+    def get(self, url, **kwargs):
+        return self.request("GET", url, **kwargs)
+
+    def post(self, url, **kwargs):
+        return self.request("POST", url, **kwargs)
+
+    def put(self, url, **kwargs):
+        return self.request("PUT", url, **kwargs)
+
+    def patch(self, url, **kwargs):
+        return self.request("PATCH", url, **kwargs)
+
+    def delete(self, url, **kwargs):
+        return self.request("DELETE", url, **kwargs)
+
+    def head(self, url, **kwargs):
+        return self.request("HEAD", url, **kwargs)
+
+    def options(self, url, **kwargs):
+        return self.request("OPTIONS", url, **kwargs)
+
+    def ws_connect(self, url, ssl=None):
+        return _WSRequestContextManager(self, self._ws_connect(url, ssl=ssl))
+
+    async def _ws_connect(self, url, ssl=None):
+        ws_client = WebSocketClient(self._base_headers.copy())
+        await ws_client.connect(url, ssl=ssl, handshake_request=self.request_raw)
+        self._reader = ws_client.reader
+        return ClientWebSocketResponse(ws_client)
diff --git a/python-ecosys/aiohttp/aiohttp/aiohttp_ws.py b/python-ecosys/aiohttp/aiohttp/aiohttp_ws.py
new file mode 100644
index 000000000..53a640fe5
--- /dev/null
+++ b/python-ecosys/aiohttp/aiohttp/aiohttp_ws.py
@@ -0,0 +1,269 @@
+# MicroPython aiohttp library
+# MIT license; Copyright (c) 2023 Carlos Gil
+# adapted from https://github.com/danni/uwebsockets
+# and https://github.com/miguelgrinberg/microdot/blob/main/src/microdot_asyncio_websocket.py
+
+import asyncio
+import random
+import json as _json
+import binascii
+import re
+import struct
+from collections import namedtuple
+
+URL_RE = re.compile(r"(wss|ws)://([A-Za-z0-9-\.]+)(?:\:([0-9]+))?(/.+)?")
+URI = namedtuple("URI", ("protocol", "hostname", "port", "path"))  # noqa: PYI024
+
+
+def urlparse(uri):
+    """Parse ws:// URLs"""
+    match = URL_RE.match(uri)
+    if match:
+        protocol = match.group(1)
+        host = match.group(2)
+        port = match.group(3)
+        path = match.group(4)
+
+        if protocol == "wss":
+            if port is None:
+                port = 443
+        elif protocol == "ws":
+            if port is None:
+                port = 80
+        else:
+            raise ValueError("Scheme {} is invalid".format(protocol))
+
+        return URI(protocol, host, int(port), path)
+
+
+class WebSocketMessage:
+    def __init__(self, opcode, data):
+        self.type = opcode
+        self.data = data
+
+
+class WSMsgType:
+    TEXT = 1
+    BINARY = 2
+    ERROR = 258
+
+
+class WebSocketClient:
+    CONT = 0
+    TEXT = 1
+    BINARY = 2
+    CLOSE = 8
+    PING = 9
+    PONG = 10
+
+    def __init__(self, params):
+        self.params = params
+        self.closed = False
+        self.reader = None
+        self.writer = None
+
+    async def connect(self, uri, ssl=None, handshake_request=None):
+        uri = urlparse(uri)
+        assert uri
+        if uri.protocol == "wss":
+            if not ssl:
+                ssl = True
+        await self.handshake(uri, ssl, handshake_request)
+
+    @classmethod
+    def _parse_frame_header(cls, header):
+        byte1, byte2 = struct.unpack("!BB", header)
+
+        # Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4)
+        fin = bool(byte1 & 0x80)
+        opcode = byte1 & 0x0F
+
+        # Byte 2: MASK(1) LENGTH(7)
+        mask = bool(byte2 & (1 << 7))
+        length = byte2 & 0x7F
+
+        return fin, opcode, mask, length
+
+    def _process_websocket_frame(self, opcode, payload):
+        if opcode == self.TEXT:
+            payload = str(payload, "utf-8")
+        elif opcode == self.BINARY:
+            pass
+        elif opcode == self.CLOSE:
+            # raise OSError(32, "Websocket connection closed")
+            return opcode, payload
+        elif opcode == self.PING:
+            return self.PONG, payload
+        elif opcode == self.PONG:  # pragma: no branch
+            return None, None
+        return None, payload
+
+    @classmethod
+    def _encode_websocket_frame(cls, opcode, payload):
+        if opcode == cls.TEXT:
+            payload = payload.encode()
+
+        length = len(payload)
+        fin = mask = True
+
+        # Frame header
+        # Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4)
+        byte1 = 0x80 if fin else 0
+        byte1 |= opcode
+
+        # Byte 2: MASK(1) LENGTH(7)
+        byte2 = 0x80 if mask else 0
+
+        if length < 126:  # 126 is magic value to use 2-byte length header
+            byte2 |= length
+            frame = struct.pack("!BB", byte1, byte2)
+
+        elif length < (1 << 16):  # Length fits in 2-bytes
+            byte2 |= 126  # Magic code
+            frame = struct.pack("!BBH", byte1, byte2, length)
+
+        elif length < (1 << 64):
+            byte2 |= 127  # Magic code
+            frame = struct.pack("!BBQ", byte1, byte2, length)
+
+        else:
+            raise ValueError
+
+        # Mask is 4 bytes
+        mask_bits = struct.pack("!I", random.getrandbits(32))
+        frame += mask_bits
+        payload = bytes(b ^ mask_bits[i % 4] for i, b in enumerate(payload))
+        return frame + payload
+
+    async def handshake(self, uri, ssl, req):
+        headers = self.params
+        _http_proto = "http" if uri.protocol != "wss" else "https"
+        url = f"{_http_proto}://{uri.hostname}:{uri.port}{uri.path or '/'}"
+        key = binascii.b2a_base64(bytes(random.getrandbits(8) for _ in range(16)))[:-1]
+        headers["Host"] = f"{uri.hostname}:{uri.port}"
+        headers["Connection"] = "Upgrade"
+        headers["Upgrade"] = "websocket"
+        headers["Sec-WebSocket-Key"] = str(key, "utf-8")
+        headers["Sec-WebSocket-Version"] = "13"
+        headers["Origin"] = f"{_http_proto}://{uri.hostname}:{uri.port}"
+
+        self.reader, self.writer = await req(
+            "GET",
+            url,
+            ssl=ssl,
+            headers=headers,
+            is_handshake=True,
+            version="HTTP/1.1",
+        )
+
+        header = await self.reader.readline()
+        header = header[:-2]
+        assert header.startswith(b"HTTP/1.1 101 "), header
+
+        while header:
+            header = await self.reader.readline()
+            header = header[:-2]
+
+    async def receive(self):
+        while True:
+            opcode, payload = await self._read_frame()
+            send_opcode, data = self._process_websocket_frame(opcode, payload)
+            if send_opcode:  # pragma: no cover
+                await self.send(data, send_opcode)
+            if opcode == self.CLOSE:
+                self.closed = True
+                return opcode, data
+            elif data:  # pragma: no branch
+                return opcode, data
+
+    async def send(self, data, opcode=None):
+        frame = self._encode_websocket_frame(
+            opcode or (self.TEXT if isinstance(data, str) else self.BINARY), data
+        )
+        self.writer.write(frame)
+        await self.writer.drain()
+
+    async def close(self):
+        if not self.closed:  # pragma: no cover
+            self.closed = True
+            await self.send(b"", self.CLOSE)
+
+    async def _read_frame(self):
+        header = await self.reader.readexactly(2)
+        if len(header) != 2:  # pragma: no cover
+            # raise OSError(32, "Websocket connection closed")
+            opcode = self.CLOSE
+            payload = b""
+            return opcode, payload
+        fin, opcode, has_mask, length = self._parse_frame_header(header)
+        if length == 126:  # Magic number, length header is 2 bytes
+            (length,) = struct.unpack("!H", await self.reader.readexactly(2))
+        elif length == 127:  # Magic number, length header is 8 bytes
+            (length,) = struct.unpack("!Q", await self.reader.readexactly(8))
+
+        if has_mask:  # pragma: no cover
+            mask = await self.reader.readexactly(4)
+        payload = await self.reader.readexactly(length)
+        if has_mask:  # pragma: no cover
+            payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
+        return opcode, payload
+
+
+class ClientWebSocketResponse:
+    def __init__(self, wsclient):
+        self.ws = wsclient
+
+    def __aiter__(self):
+        return self
+
+    async def __anext__(self):
+        msg = WebSocketMessage(*await self.ws.receive())
+        # print(msg.data, msg.type) # DEBUG
+        if (not msg.data and msg.type == self.ws.CLOSE) or self.ws.closed:
+            raise StopAsyncIteration
+        return msg
+
+    async def close(self):
+        await self.ws.close()
+
+    async def send_str(self, data):
+        if not isinstance(data, str):
+            raise TypeError("data argument must be str (%r)" % type(data))
+        await self.ws.send(data)
+
+    async def send_bytes(self, data):
+        if not isinstance(data, (bytes, bytearray, memoryview)):
+            raise TypeError("data argument must be byte-ish (%r)" % type(data))
+        await self.ws.send(data)
+
+    async def send_json(self, data):
+        await self.send_str(_json.dumps(data))
+
+    async def receive_str(self):
+        msg = WebSocketMessage(*await self.ws.receive())
+        if msg.type != self.ws.TEXT:
+            raise TypeError(f"Received message {msg.type}:{msg.data!r} is not str")
+        return msg.data
+
+    async def receive_bytes(self):
+        msg = WebSocketMessage(*await self.ws.receive())
+        if msg.type != self.ws.BINARY:
+            raise TypeError(f"Received message {msg.type}:{msg.data!r} is not bytes")
+        return msg.data
+
+    async def receive_json(self):
+        data = await self.receive_str()
+        return _json.loads(data)
+
+
+class _WSRequestContextManager:
+    def __init__(self, client, request_co):
+        self.reqco = request_co
+        self.client = client
+
+    async def __aenter__(self):
+        return await self.reqco
+
+    async def __aexit__(self, *args):
+        await self.client._reader.aclose()
+        return await asyncio.sleep(0)
diff --git a/python-ecosys/aiohttp/examples/client.py b/python-ecosys/aiohttp/examples/client.py
new file mode 100644
index 000000000..0a6476064
--- /dev/null
+++ b/python-ecosys/aiohttp/examples/client.py
@@ -0,0 +1,19 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.insert(0, ".")
+import aiohttp
+import asyncio
+
+
+async def main():
+    async with aiohttp.ClientSession() as session:
+        async with session.get("http://micropython.org") as response:
+            print("Status:", response.status)
+            print("Content-Type:", response.headers["Content-Type"])
+
+            html = await response.text()
+            print("Body:", html[:15], "...")
+
+
+asyncio.run(main())
diff --git a/python-ecosys/aiohttp/examples/compression.py b/python-ecosys/aiohttp/examples/compression.py
new file mode 100644
index 000000000..a1c6276b2
--- /dev/null
+++ b/python-ecosys/aiohttp/examples/compression.py
@@ -0,0 +1,21 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.insert(0, ".")
+import aiohttp
+import asyncio
+
+headers = {"Accept-Encoding": "gzip,deflate"}
+
+
+async def main():
+    async with aiohttp.ClientSession(headers=headers, version=aiohttp.HttpVersion11) as session:
+        async with session.get("http://micropython.org") as response:
+            print("Status:", response.status)
+            print("Content-Type:", response.headers["Content-Type"])
+            print(response.headers)
+            html = await response.text()
+            print(html)
+
+
+asyncio.run(main())
diff --git a/python-ecosys/aiohttp/examples/get.py b/python-ecosys/aiohttp/examples/get.py
new file mode 100644
index 000000000..087d6fb51
--- /dev/null
+++ b/python-ecosys/aiohttp/examples/get.py
@@ -0,0 +1,30 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.insert(0, ".")
+import aiohttp
+import asyncio
+
+
+URL = sys.argv.pop()
+
+if not URL.startswith("http"):
+    URL = "http://micropython.org"
+
+print(URL)
+
+
+async def fetch(client):
+    async with client.get(URL) as resp:
+        assert resp.status == 200
+        return await resp.text()
+
+
+async def main():
+    async with aiohttp.ClientSession() as client:
+        html = await fetch(client)
+        print(html)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/python-ecosys/aiohttp/examples/headers.py b/python-ecosys/aiohttp/examples/headers.py
new file mode 100644
index 000000000..ec5c00a80
--- /dev/null
+++ b/python-ecosys/aiohttp/examples/headers.py
@@ -0,0 +1,19 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.insert(0, ".")
+import aiohttp
+import asyncio
+
+
+headers = {"Authorization": "Basic bG9naW46cGFzcw=="}
+
+
+async def main():
+    async with aiohttp.ClientSession(headers=headers) as session:
+        async with session.get("http://httpbin.org/headers") as r:
+            json_body = await r.json()
+            print(json_body)
+
+
+asyncio.run(main())
diff --git a/python-ecosys/aiohttp/examples/methods.py b/python-ecosys/aiohttp/examples/methods.py
new file mode 100644
index 000000000..af38ff652
--- /dev/null
+++ b/python-ecosys/aiohttp/examples/methods.py
@@ -0,0 +1,26 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.insert(0, ".")
+import aiohttp
+import asyncio
+
+
+async def main():
+    async with aiohttp.ClientSession("http://httpbin.org") as session:
+        async with session.get("/get") as resp:
+            assert resp.status == 200
+            rget = await resp.text()
+            print(f"GET: {rget}")
+        async with session.post("/post", json={"foo": "bar"}) as resp:
+            assert resp.status == 200
+            rpost = await resp.text()
+            print(f"POST: {rpost}")
+        async with session.put("/put", data=b"data") as resp:
+            assert resp.status == 200
+            rput = await resp.json()
+            print("PUT: ", rput)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/python-ecosys/aiohttp/examples/params.py b/python-ecosys/aiohttp/examples/params.py
new file mode 100644
index 000000000..9aecb2ab8
--- /dev/null
+++ b/python-ecosys/aiohttp/examples/params.py
@@ -0,0 +1,21 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.insert(0, ".")
+import aiohttp
+import asyncio
+
+
+params = {"key1": "value1", "key2": "value2"}
+
+
+async def main():
+    async with aiohttp.ClientSession() as session:
+        async with session.get("http://httpbin.org/get", params=params) as response:
+            expect = "http://httpbin.org/get?key1=value1&key2=value2"
+            assert str(response.url) == expect, f"{response.url} != {expect}"
+            html = await response.text()
+            print(html)
+
+
+asyncio.run(main())
diff --git a/python-ecosys/aiohttp/examples/ws.py b/python-ecosys/aiohttp/examples/ws.py
new file mode 100644
index 000000000..b96ee6819
--- /dev/null
+++ b/python-ecosys/aiohttp/examples/ws.py
@@ -0,0 +1,45 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.insert(0, ".")
+import aiohttp
+import asyncio
+
+try:
+    URL = sys.argv[1]  # expects a websocket echo server
+except Exception:
+    URL = "ws://echo.websocket.events"
+
+
+sslctx = False
+
+if URL.startswith("wss:"):
+    try:
+        import ssl
+
+        sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+        sslctx.verify_mode = ssl.CERT_NONE
+    except Exception:
+        pass
+
+
+async def ws_test_echo(session):
+    async with session.ws_connect(URL, ssl=sslctx) as ws:
+        await ws.send_str("hello world!\r\n")
+        async for msg in ws:
+            if msg.type == aiohttp.WSMsgType.TEXT:
+                print(msg.data)
+
+            if "close" in msg.data:
+                break
+            await ws.send_str("close\r\n")
+        await ws.close()
+
+
+async def main():
+    async with aiohttp.ClientSession() as session:
+        await ws_test_echo(session)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/python-ecosys/aiohttp/examples/ws_repl_echo.py b/python-ecosys/aiohttp/examples/ws_repl_echo.py
new file mode 100644
index 000000000..c41a4ee5e
--- /dev/null
+++ b/python-ecosys/aiohttp/examples/ws_repl_echo.py
@@ -0,0 +1,54 @@
+import sys
+
+# ruff: noqa: E402
+sys.path.insert(0, ".")
+import aiohttp
+import asyncio
+
+try:
+    URL = sys.argv[1]  # expects a websocket echo server
+    READ_BANNER = False
+except Exception:
+    URL = "ws://echo.websocket.events"
+    READ_BANNER = True
+
+
+sslctx = False
+
+if URL.startswith("wss:"):
+    try:
+        import ssl
+
+        sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+        sslctx.verify_mode = ssl.CERT_NONE
+    except Exception:
+        pass
+
+
+async def ws_test_echo(session):
+    async with session.ws_connect(URL, ssl=sslctx) as ws:
+        if READ_BANNER:
+            print(await ws.receive_str())
+        try:
+            while True:
+                await ws.send_str(f"{input('>>> ')}\r\n")
+
+                async for msg in ws:
+                    if msg.type == aiohttp.WSMsgType.TEXT:
+                        print(msg.data, end="")
+                        break
+
+        except KeyboardInterrupt:
+            pass
+
+        finally:
+            await ws.close()
+
+
+async def main():
+    async with aiohttp.ClientSession() as session:
+        await ws_test_echo(session)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/python-ecosys/aiohttp/manifest.py b/python-ecosys/aiohttp/manifest.py
new file mode 100644
index 000000000..bbf22bb29
--- /dev/null
+++ b/python-ecosys/aiohttp/manifest.py
@@ -0,0 +1,7 @@
+metadata(
+    description="HTTP client module for MicroPython asyncio module",
+    version="0.0.6",
+    pypi="aiohttp",
+)
+
+package("aiohttp")
diff --git a/python-ecosys/cbor2/cbor2/__init__.py b/python-ecosys/cbor2/cbor2/__init__.py
new file mode 100644
index 000000000..80790f0da
--- /dev/null
+++ b/python-ecosys/cbor2/cbor2/__init__.py
@@ -0,0 +1,32 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from ._decoder import CBORDecoder
+from ._decoder import load
+from ._decoder import loads
+
+from ._encoder import CBOREncoder
+from ._encoder import dump
+from ._encoder import dumps
diff --git a/python-ecosys/cbor2/cbor2/_decoder.py b/python-ecosys/cbor2/cbor2/_decoder.py
new file mode 100644
index 000000000..965dbfd46
--- /dev/null
+++ b/python-ecosys/cbor2/cbor2/_decoder.py
@@ -0,0 +1,262 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import io
+import struct
+
+
+class CBORDecodeError(Exception):
+    """Raised when an error occurs deserializing a CBOR datastream."""
+
+
+break_marker = object()
+
+
+class CBORSimpleValue(object):
+    """
+    Represents a CBOR "simple value".
+    :param int value: the value (0-255)
+    """
+
+    def __init__(self, value):
+        if value < 0 or value > 255:
+            raise TypeError("simple value too big")
+        self.value = value
+
+    def __eq__(self, other):
+        if isinstance(other, CBORSimpleValue):
+            return self.value == other.value
+        elif isinstance(other, int):
+            return self.value == other
+        return NotImplemented
+
+    def __repr__(self):
+        return "CBORSimpleValue({self.value})".format(self=self)
+
+
+def decode_uint(decoder, subtype, allow_indefinite=False):
+    # Major tag 0
+    if subtype < 24:
+        return subtype
+    elif subtype == 24:
+        return struct.unpack(">B", decoder.read(1))[0]
+    elif subtype == 25:
+        return struct.unpack(">H", decoder.read(2))[0]
+    elif subtype == 26:
+        return struct.unpack(">L", decoder.read(4))[0]
+    elif subtype == 27:
+        return struct.unpack(">Q", decoder.read(8))[0]
+    elif subtype == 31 and allow_indefinite:
+        return None
+    else:
+        raise CBORDecodeError("unknown unsigned integer subtype 0x%x" % subtype)
+
+
+def decode_negint(decoder, subtype):
+    # Major tag 1
+    uint = decode_uint(decoder, subtype)
+    return -uint - 1
+
+
+def decode_bytestring(decoder, subtype):
+    # Major tag 2
+    length = decode_uint(decoder, subtype, allow_indefinite=True)
+    if length is None:
+        # Indefinite length
+        buf = bytearray()
+        while True:
+            initial_byte = decoder.read(1)[0]
+            if initial_byte == 255:
+                return buf
+            else:
+                length = decode_uint(decoder, initial_byte & 31)
+                value = decoder.read(length)
+                buf.extend(value)
+    else:
+        return decoder.read(length)
+
+
+def decode_string(decoder, subtype):
+    # Major tag 3
+    return decode_bytestring(decoder, subtype).decode("utf-8")
+
+
+def decode_array(decoder, subtype):
+    # Major tag 4
+    items = []
+    length = decode_uint(decoder, subtype, allow_indefinite=True)
+    if length is None:
+        # Indefinite length
+        while True:
+            value = decoder.decode()
+            if value is break_marker:
+                break
+            else:
+                items.append(value)
+    else:
+        for _ in range(length):
+            item = decoder.decode()
+            items.append(item)
+    return items
+
+
+def decode_map(decoder, subtype):
+    # Major tag 5
+    dictionary = {}
+    length = decode_uint(decoder, subtype, allow_indefinite=True)
+    if length is None:
+        # Indefinite length
+        while True:
+            key = decoder.decode()
+            if key is break_marker:
+                break
+            else:
+                value = decoder.decode()
+                dictionary[key] = value
+    else:
+        for _ in range(length):
+            key = decoder.decode()
+            value = decoder.decode()
+            dictionary[key] = value
+
+    return dictionary
+
+
+def decode_special(decoder, subtype):
+    # Simple value
+    if subtype < 20:
+        return CBORSimpleValue(subtype)
+
+    # Major tag 7
+    return special_decoders[subtype](decoder)
+
+
+def decode_simple_value(decoder):
+    return CBORSimpleValue(struct.unpack(">B", decoder.read(1))[0])
+
+
+def decode_float16(decoder):
+    decoder.read(2)
+    raise NotImplementedError  # no float16 unpack function
+
+
+def decode_float32(decoder):
+    return struct.unpack(">f", decoder.read(4))[0]
+
+
+def decode_float64(decoder):
+    return struct.unpack(">d", decoder.read(8))[0]
+
+
+major_decoders = {
+    0: decode_uint,
+    1: decode_negint,
+    2: decode_bytestring,
+    3: decode_string,
+    4: decode_array,
+    5: decode_map,
+    7: decode_special,
+}
+
+special_decoders = {
+    20: lambda self: False,
+    21: lambda self: True,
+    22: lambda self: None,
+    # 23 is undefined
+    24: decode_simple_value,
+    25: decode_float16,
+    26: decode_float32,
+    27: decode_float64,
+    31: lambda self: break_marker,
+}
+
+
+class CBORDecoder(object):
+    """
+    Deserializes a CBOR encoded byte stream.
+    """
+
+    def __init__(self, fp):
+        self.fp = fp
+
+    def read(self, amount):
+        """
+        Read bytes from the data stream.
+        :param int amount: the number of bytes to read
+        """
+        data = self.fp.read(amount)
+        if len(data) < amount:
+            raise CBORDecodeError(
+                "premature end of stream (expected to read {} bytes, got {} instead)".format(
+                    amount, len(data)
+                )
+            )
+
+        return data
+
+    def decode(self):
+        """
+        Decode the next value from the stream.
+        :raises CBORDecodeError: if there is any problem decoding the stream
+        """
+        try:
+            initial_byte = self.fp.read(1)[0]
+            major_type = initial_byte >> 5
+            subtype = initial_byte & 31
+        except Exception as e:
+            raise CBORDecodeError(
+                "error reading major type at index {}: {}".format(self.fp.tell(), e)
+            )
+
+        decoder = major_decoders[major_type]
+        try:
+            return decoder(self, subtype)
+        except CBORDecodeError:
+            raise
+        except Exception as e:
+            raise CBORDecodeError(
+                "error decoding value {}".format(e)
+            )  # tell doesn't work on micropython at the moment
+
+
+def loads(payload, **kwargs):
+    """
+    Deserialize an object from a bytestring.
+    :param bytes payload: the bytestring to serialize
+    :param kwargs: keyword arguments passed to :class:`~.CBORDecoder`
+    :return: the deserialized object
+    """
+    fp = io.BytesIO(payload)
+    return CBORDecoder(fp, **kwargs).decode()
+
+
+def load(fp, **kwargs):
+    """
+    Deserialize an object from an open file.
+    :param fp: the input file (any file-like object)
+    :param kwargs: keyword arguments passed to :class:`~.CBORDecoder`
+    :return: the deserialized object
+    """
+    return CBORDecoder(fp, **kwargs).decode()
diff --git a/python-ecosys/cbor2/cbor2/_encoder.py b/python-ecosys/cbor2/cbor2/_encoder.py
new file mode 100644
index 000000000..fe8715468
--- /dev/null
+++ b/python-ecosys/cbor2/cbor2/_encoder.py
@@ -0,0 +1,182 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import io
+import struct
+
+
+class CBOREncodeError(Exception):
+    """Raised when an error occurs while serializing an object into a CBOR datastream."""
+
+
+def encode_length(major_tag, length):
+    if length < 24:
+        return struct.pack(">B", major_tag | length)
+    elif length < 256:
+        return struct.pack(">BB", major_tag | 24, length)
+    elif length < 65536:
+        return struct.pack(">BH", major_tag | 25, length)
+    elif length < 4294967296:
+        return struct.pack(">BL", major_tag | 26, length)
+    else:
+        return struct.pack(">BQ", major_tag | 27, length)
+
+
+def encode_semantic(encoder, tag, value):
+    encoder.write(encode_length(0xC0, tag))
+    encoder.encode(value)
+
+
+def encode_float(encoder, value):
+    # Handle special values efficiently
+    import math
+
+    if math.isnan(value):
+        encoder.write(b"\xf9\x7e\x00")
+    elif math.isinf(value):
+        encoder.write(b"\xf9\x7c\x00" if value > 0 else b"\xf9\xfc\x00")
+    else:
+        encoder.write(struct.pack(">Bd", 0xFB, value))
+
+
+def encode_int(encoder, value):
+    # Big integers (2 ** 64 and over)
+    if value >= 18446744073709551616 or value < -18446744073709551616:
+        if value >= 0:
+            major_type = 0x02
+        else:
+            major_type = 0x03
+            value = -value - 1
+
+        values = []
+        while value > 0:
+            value, remainder = divmod(value, 256)
+            values.insert(0, remainder)
+
+        payload = bytes(values)
+        encode_semantic(encoder, major_type, payload)
+    elif value >= 0:
+        encoder.write(encode_length(0, value))
+    else:
+        encoder.write(encode_length(0x20, abs(value) - 1))
+
+
+def encode_bytestring(encoder, value):
+    encoder.write(encode_length(0x40, len(value)) + value)
+
+
+def encode_bytearray(encoder, value):
+    encode_bytestring(encoder, bytes(value))
+
+
+def encode_string(encoder, value):
+    encoded = value.encode("utf-8")
+    encoder.write(encode_length(0x60, len(encoded)) + encoded)
+
+
+def encode_map(encoder, value):
+    encoder.write(encode_length(0xA0, len(value)))
+    for key, val in value.items():
+        encoder.encode(key)
+        encoder.encode(val)
+
+
+def encode_array(encoder, value):
+    encoder.write(encode_length(0x80, len(value)))
+    for item in value:
+        encoder.encode(item)
+
+
+def encode_boolean(encoder, value):
+    encoder.write(b"\xf5" if value else b"\xf4")
+
+
+def encode_none(encoder, value):
+    encoder.write(b"\xf6")
+
+
+cbor_encoders = {  # supported data types and the encoder to use.
+    bytes: encode_bytestring,
+    bytearray: encode_bytearray,
+    str: encode_string,
+    int: encode_int,
+    float: encode_float,
+    bool: encode_boolean,
+    type(None): encode_none,
+    list: encode_array,
+    dict: encode_map,
+}
+
+
+class CBOREncoder(object):
+    """
+    Serializes objects to a byte stream using Concise Binary Object Representation.
+    """
+
+    def __init__(self, fp):
+        self.fp = fp
+
+    def _find_encoder(self, obj):
+        return cbor_encoders[type(obj)]
+
+    def write(self, data):
+        """
+        Write bytes to the data stream.
+        :param data: the bytes to write
+        """
+        self.fp.write(data)
+
+    def encode(self, obj):
+        """
+        Encode the given object using CBOR.
+        :param obj: the object to encode
+        """
+        encoder = self._find_encoder(obj)
+        if not encoder:
+            raise CBOREncodeError("cannot serialize type %s" % type(obj))
+        encoder(self, obj)
+
+
+def dumps(obj, **kwargs):
+    """
+    Serialize an object to a bytestring.
+    :param obj: the object to serialize
+    :param kwargs: keyword arguments passed to :class:`~.CBOREncoder`
+    :return: the serialized output
+    :rtype: bytes
+    """
+    fp = io.BytesIO()
+    dump(obj, fp, **kwargs)
+    return fp.getvalue()
+
+
+def dump(obj, fp, **kwargs):
+    """
+    Serialize an object to a file.
+    :param obj: the object to serialize
+    :param fp: a file-like object
+    :param kwargs: keyword arguments passed to :class:`~.CBOREncoder`
+    """
+    CBOREncoder(fp, **kwargs).encode(obj)
diff --git a/python-ecosys/cbor2/examples/cbor_test.py b/python-ecosys/cbor2/examples/cbor_test.py
new file mode 100644
index 000000000..a1cd7e93e
--- /dev/null
+++ b/python-ecosys/cbor2/examples/cbor_test.py
@@ -0,0 +1,37 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2023 Arduino SA
+Copyright (c) 2018 KPN (Jan Bogaerts)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import cbor2
+
+input = [
+    {"bn": "urn:dev:ow:10e2073a01080063", "u": "Cel", "t": 1.276020076e09, "v": 23.5},
+    {"u": "Cel", "t": 1.276020091e09, "v": 23.6},
+]
+
+data = cbor2.dumps(input)
+print(data)
+print(data.hex())
+text = cbor2.loads(data)
+print(text)
diff --git a/python-ecosys/cbor2/manifest.py b/python-ecosys/cbor2/manifest.py
new file mode 100644
index 000000000..aa4b77092
--- /dev/null
+++ b/python-ecosys/cbor2/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="1.0.0", pypi="cbor2")
+
+package("cbor2")
diff --git a/python-ecosys/iperf3/iperf3.py b/python-ecosys/iperf3/iperf3.py
new file mode 100644
index 000000000..363d10d59
--- /dev/null
+++ b/python-ecosys/iperf3/iperf3.py
@@ -0,0 +1,555 @@
+"""
+Pure Python, iperf3-compatible network performance test tool.
+
+MIT license; Copyright (c) 2018-2019 Damien P. George
+
+Supported modes: server & client, TCP & UDP, normal & reverse
+
+Usage:
+    import iperf3
+    iperf3.server()
+    iperf3.client('192.168.1.5')
+    iperf3.client('192.168.1.5', udp=True, reverse=True)
+"""
+
+import json
+import select
+import socket
+import struct
+import sys
+import time
+
+# Provide a urandom() function, supporting devices without os.urandom().
+try:
+    from os import urandom
+except ImportError:
+    from random import randint
+
+    def urandom(n):
+        return bytes(randint(0, 255) for _ in range(n))
+
+
+DEBUG = False
+
+# iperf3 cookie size, last byte is null byte
+COOKIE_SIZE = 37
+
+# iperf3 commands
+TEST_START = 1
+TEST_RUNNING = 2
+TEST_END = 4
+PARAM_EXCHANGE = 9
+CREATE_STREAMS = 10
+EXCHANGE_RESULTS = 13
+DISPLAY_RESULTS = 14
+IPERF_DONE = 16
+
+if DEBUG:
+    cmd_string = {
+        TEST_START: "TEST_START",
+        TEST_RUNNING: "TEST_RUNNING",
+        TEST_END: "TEST_END",
+        PARAM_EXCHANGE: "PARAM_EXCHANGE",
+        CREATE_STREAMS: "CREATE_STREAMS",
+        EXCHANGE_RESULTS: "EXCHANGE_RESULTS",
+        DISPLAY_RESULTS: "DISPLAY_RESULTS",
+        IPERF_DONE: "IPERF_DONE",
+    }
+
+
+def fmt_size(val, div):
+    for mult in ("", "K", "M", "G"):
+        if val < 10:
+            return "% 5.2f %s" % (val, mult)
+        elif val < 100:
+            return "% 5.1f %s" % (val, mult)
+        elif mult == "G" or val < 1000:
+            return "% 5.0f %s" % (val, mult)
+        else:
+            val /= div
+
+
+class Stats:
+    def __init__(self, param):
+        self.pacing_timer_us = param["pacing_timer"] * 1000
+        self.udp = param.get("udp", False)
+        self.reverse = param.get("reverse", False)
+        self.running = False
+
+    def start(self):
+        self.running = True
+        self.t0 = self.t1 = ticks_us()
+        self.nb0 = self.nb1 = 0  # num bytes
+        self.np0 = self.np1 = 0  # num packets
+        self.nm0 = self.nm1 = 0  # num lost packets
+        if self.udp:
+            if self.reverse:
+                extra = "         Jitter    Lost/Total Datagrams"
+            else:
+                extra = "         Total Datagrams"
+        else:
+            extra = ""
+        print("Interval           Transfer     Bitrate" + extra)
+
+    def max_dt_ms(self):
+        if not self.running:
+            return -1
+        return max(0, (self.pacing_timer_us - ticks_diff(ticks_us(), self.t1)) // 1000)
+
+    def add_bytes(self, n):
+        if not self.running:
+            return
+        self.nb0 += n
+        self.nb1 += n
+        self.np0 += 1
+        self.np1 += 1
+
+    def add_lost_packets(self, n):
+        self.np0 += n
+        self.np1 += n
+        self.nm0 += n
+        self.nm1 += n
+
+    def print_line(self, ta, tb, nb, np, nm, extra=""):
+        dt = tb - ta
+        print(
+            " %5.2f-%-5.2f  sec %sBytes %sbits/sec"
+            % (ta, tb, fmt_size(nb, 1024), fmt_size(nb * 8 / dt, 1000)),
+            end="",
+        )
+        if self.udp:
+            if self.reverse:
+                print(
+                    " %6.3f ms  %u/%u (%.1f%%)" % (0, nm, np, 100 * nm / (max(1, np + nm))), end=""
+                )
+            else:
+                print("  %u" % np, end="")
+        print(extra)
+
+    def update(self, final=False):
+        if not self.running:
+            return
+        t2 = ticks_us()
+        dt = ticks_diff(t2, self.t1)
+        if final or dt > self.pacing_timer_us:
+            ta = ticks_diff(self.t1, self.t0) * 1e-6
+            tb = ticks_diff(t2, self.t0) * 1e-6
+            self.print_line(ta, tb, self.nb1, self.np1, self.nm1)
+            self.t1 = t2
+            self.nb1 = 0
+            self.np1 = 0
+            self.nm1 = 0
+
+    def stop(self):
+        self.update(True)
+        self.running = False
+        self.t3 = ticks_us()
+        dt = ticks_diff(self.t3, self.t0)
+        print("- " * 30)
+        self.print_line(0, dt * 1e-6, self.nb0, self.np0, self.nm0, "  sender")
+
+    def report_receiver(self, stats):
+        st = stats["streams"][0]
+
+        # iperf servers pre 3.2 do not transmit start or end time,
+        # so use local as fallback if not available.
+        dt = ticks_diff(self.t3, self.t0)
+
+        self.print_line(
+            st.get("start_time", 0.0),
+            st.get("end_time", dt * 1e-6),
+            st["bytes"],
+            st["packets"],
+            st["errors"],
+            "  receiver",
+        )
+
+
+def recvn(s, n):
+    data = b""
+    while len(data) < n:
+        data += s.recv(n - len(data))
+    return data
+
+
+def recvinto(s, buf):
+    if hasattr(s, "readinto"):
+        return s.readinto(buf)
+    else:
+        return s.recv_into(buf)
+
+
+def recvninto(s, buf):
+    if hasattr(s, "readinto"):
+        n = s.readinto(buf)
+        assert n == len(buf)
+    else:
+        mv = memoryview(buf)
+        off = 0
+        while off < len(buf):
+            off += s.recv_into(mv[off:])
+
+
+def make_cookie():
+    cookie_chars = b"abcdefghijklmnopqrstuvwxyz234567"
+    cookie = bytearray(COOKIE_SIZE)
+    for i, x in enumerate(urandom(COOKIE_SIZE - 1)):
+        cookie[i] = cookie_chars[x & 31]
+    return cookie
+
+
+def server_once():
+    # Listen for a connection
+    ai = socket.getaddrinfo("0.0.0.0", 5201)
+    ai = ai[0]
+    print("Server listening on", ai[-1])
+    s_listen = socket.socket(ai[0], socket.SOCK_STREAM)
+    s_listen.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    s_listen.bind(ai[-1])
+    s_listen.listen(1)
+    s_ctrl, addr = s_listen.accept()
+
+    # Read client's cookie
+    cookie = recvn(s_ctrl, COOKIE_SIZE)
+    if DEBUG:
+        print(cookie)
+
+    # Ask for parameters
+    s_ctrl.sendall(bytes([PARAM_EXCHANGE]))
+
+    # Get parameters
+    n = struct.unpack(">I", recvn(s_ctrl, 4))[0]
+    param = recvn(s_ctrl, n)
+    param = json.loads(str(param, "ascii"))
+    if DEBUG:
+        print(param)
+    reverse = param.get("reverse", False)
+
+    # Ask to create streams
+    s_ctrl.sendall(bytes([CREATE_STREAMS]))
+
+    if param.get("tcp", False):
+        # Accept stream
+        s_data, addr = s_listen.accept()
+        print("Accepted connection:", addr)
+        recvn(s_data, COOKIE_SIZE)
+    elif param.get("udp", False):
+        # Close TCP connection and open UDP "connection"
+        s_listen.close()
+        s_data = socket.socket(ai[0], socket.SOCK_DGRAM)
+        s_data.bind(ai[-1])
+        data, addr = s_data.recvfrom(4)
+        s_data.sendto(b"\x12\x34\x56\x78", addr)
+    else:
+        assert False
+
+    # Start test
+    s_ctrl.sendall(bytes([TEST_START]))
+
+    # Run test
+    s_ctrl.sendall(bytes([TEST_RUNNING]))
+
+    # Read data, and wait for client to send TEST_END
+    poll = select.poll()
+    poll.register(s_ctrl, select.POLLIN)
+    if reverse:
+        poll.register(s_data, select.POLLOUT)
+    else:
+        poll.register(s_data, select.POLLIN)
+    stats = Stats(param)
+    stats.start()
+    running = True
+    data_buf = bytearray(urandom(param["len"]))
+    while running:
+        for pollable in poll.poll(stats.max_dt_ms()):
+            if pollable_is_sock(pollable, s_ctrl):
+                cmd = recvn(s_ctrl, 1)[0]
+                if DEBUG:
+                    print(cmd_string.get(cmd, "UNKNOWN_COMMAND"))
+                if cmd == TEST_END:
+                    running = False
+            elif pollable_is_sock(pollable, s_data):
+                if reverse:
+                    n = s_data.send(data_buf)
+                    stats.add_bytes(n)
+                else:
+                    recvninto(s_data, data_buf)
+                    stats.add_bytes(len(data_buf))
+        stats.update()
+
+    # Need to continue writing so other side doesn't get blocked waiting for data
+    if reverse:
+        while True:
+            for pollable in poll.poll(0):
+                if pollable_is_sock(pollable, s_data):
+                    s_data.send(data_buf)
+                    break
+            else:
+                break
+
+    stats.stop()
+
+    # Ask to exchange results
+    s_ctrl.sendall(bytes([EXCHANGE_RESULTS]))
+
+    # Get client results
+    n = struct.unpack(">I", recvn(s_ctrl, 4))[0]
+    results = recvn(s_ctrl, n)
+    results = json.loads(str(results, "ascii"))
+    if DEBUG:
+        print(results)
+
+    # Send our results
+    results = {
+        "cpu_util_total": 1,
+        "cpu_util_user": 0.5,
+        "cpu_util_system": 0.5,
+        "sender_has_retransmits": 1,
+        "congestion_used": "cubic",
+        "streams": [
+            {
+                "id": 1,
+                "bytes": stats.nb0,
+                "retransmits": 0,
+                "jitter": 0,
+                "errors": 0,
+                "packets": stats.np0,
+                "start_time": 0,
+                "end_time": ticks_diff(stats.t3, stats.t0) * 1e-6,
+            }
+        ],
+    }
+    results = json.dumps(results)
+    s_ctrl.sendall(struct.pack(">I", len(results)))
+    s_ctrl.sendall(bytes(results, "ascii"))
+
+    # Ask to display results
+    s_ctrl.sendall(bytes([DISPLAY_RESULTS]))
+
+    # Wait for client to send IPERF_DONE
+    cmd = recvn(s_ctrl, 1)[0]
+    assert cmd == IPERF_DONE
+
+    # Close all sockets
+    s_data.close()
+    s_ctrl.close()
+    s_listen.close()
+
+
+def server():
+    while True:
+        server_once()
+
+
+def client(host, udp=False, reverse=False, bandwidth=10 * 1024 * 1024):
+    print("CLIENT MODE:", "UDP" if udp else "TCP", "receiving" if reverse else "sending")
+
+    param = {
+        "client_version": "3.6",
+        "omit": 0,
+        "parallel": 1,
+        "pacing_timer": 1000,
+        "time": 10,
+    }
+
+    if udp:
+        param["udp"] = True
+        param["len"] = 1500 - 42
+        param["bandwidth"] = bandwidth  # this should be should be intended bits per second
+        udp_interval = 1000000 * 8 * param["len"] // param["bandwidth"]
+    else:
+        param["tcp"] = True
+        param["len"] = 3000
+
+    if reverse:
+        param["reverse"] = True
+
+    # Connect to server
+    ai = socket.getaddrinfo(host, 5201)[0]
+    print("Connecting to", ai[-1])
+    s_ctrl = socket.socket(ai[0], socket.SOCK_STREAM)
+    s_ctrl.connect(ai[-1])
+
+    # Send our cookie
+    cookie = make_cookie()
+    if DEBUG:
+        print(cookie)
+    s_ctrl.sendall(cookie)
+
+    # Object to gather statistics about the run
+    stats = Stats(param)
+
+    # Run the main loop, waiting for incoming commands and dat
+    ticks_us_end = param["time"] * 1000000
+    poll = select.poll()
+    poll.register(s_ctrl, select.POLLIN)
+    buf = None
+    s_data = None
+    start = None
+    udp_packet_id = 0
+    udp_last_send = None
+    while True:
+        for pollable in poll.poll(stats.max_dt_ms()):
+            if pollable_is_sock(pollable, s_data):
+                # Data socket is writable/readable
+                t = ticks_us()
+                if ticks_diff(t, start) > ticks_us_end:
+                    if reverse:
+                        # Continue to drain any incoming data
+                        recvinto(s_data, buf)
+                    if stats.running:
+                        # End of run
+                        s_ctrl.sendall(bytes([TEST_END]))
+                        stats.stop()
+                else:
+                    # Send/receiver data
+                    if udp:
+                        if reverse:
+                            recvninto(s_data, buf)
+                            udp_in_sec, udp_in_usec, udp_in_id = struct.unpack_from(">III", buf, 0)
+                            # print(udp_in_sec, udp_in_usec, udp_in_id)
+                            if udp_in_id != udp_packet_id + 1:
+                                stats.add_lost_packets(udp_in_id - (udp_packet_id + 1))
+                            udp_packet_id = udp_in_id
+                            stats.add_bytes(len(buf))
+                        else:
+                            # print('UDP send', udp_last_send, t, udp_interval)
+                            if t - udp_last_send > udp_interval:
+                                udp_last_send += udp_interval
+                                udp_packet_id += 1
+                                struct.pack_into(
+                                    ">III", buf, 0, t // 1000000, t % 1000000, udp_packet_id
+                                )
+                                n = s_data.sendto(buf, ai[-1])
+                                stats.add_bytes(n)
+                    else:
+                        if reverse:
+                            recvninto(s_data, buf)
+                            n = len(buf)
+                        else:
+                            # print('TCP send', len(buf))
+                            n = s_data.send(buf)
+                        stats.add_bytes(n)
+
+            elif pollable_is_sock(pollable, s_ctrl):
+                # Receive command
+                cmd = recvn(s_ctrl, 1)[0]
+                if DEBUG:
+                    print(cmd_string.get(cmd, "UNKNOWN_COMMAND"))
+                if cmd == TEST_START:
+                    if reverse:
+                        # Start receiving data now, because data socket is open
+                        poll.register(s_data, select.POLLIN)
+                        start = ticks_us()
+                        stats.start()
+                elif cmd == TEST_RUNNING:
+                    if not reverse:
+                        # Start sending data now
+                        poll.register(s_data, select.POLLOUT)
+                        start = ticks_us()
+                        if udp:
+                            udp_last_send = start - udp_interval
+                        stats.start()
+                elif cmd == PARAM_EXCHANGE:
+                    param_j = json.dumps(param)
+                    s_ctrl.sendall(struct.pack(">I", len(param_j)))
+                    s_ctrl.sendall(bytes(param_j, "ascii"))
+                elif cmd == CREATE_STREAMS:
+                    if udp:
+                        s_data = socket.socket(ai[0], socket.SOCK_DGRAM)
+                        s_data.sendto(struct.pack("<I", 123456789), ai[-1])
+                        recvn(s_data, 4)  # get dummy response from server (=987654321)
+                    else:
+                        s_data = socket.socket(ai[0], socket.SOCK_STREAM)
+                        s_data.connect(ai[-1])
+                        s_data.sendall(cookie)
+                    buf = bytearray(urandom(param["len"]))
+                elif cmd == EXCHANGE_RESULTS:
+                    # Close data socket now that server knows we are finished, to prevent it flooding us
+                    poll.unregister(s_data)
+                    s_data.close()
+                    s_data = None
+
+                    results = {
+                        "cpu_util_total": 1,
+                        "cpu_util_user": 0.5,
+                        "cpu_util_system": 0.5,
+                        "sender_has_retransmits": 1,
+                        "congestion_used": "cubic",
+                        "streams": [
+                            {
+                                "id": 1,
+                                "bytes": stats.nb0,
+                                "retransmits": 0,
+                                "jitter": 0,
+                                "errors": stats.nm0,
+                                "packets": stats.np0,
+                                "start_time": 0,
+                                "end_time": ticks_diff(stats.t3, stats.t0) * 1e-6,
+                            }
+                        ],
+                    }
+                    results = json.dumps(results)
+                    s_ctrl.sendall(struct.pack(">I", len(results)))
+                    s_ctrl.sendall(bytes(results, "ascii"))
+
+                    n = struct.unpack(">I", recvn(s_ctrl, 4))[0]
+                    results = recvn(s_ctrl, n)
+                    results = json.loads(str(results, "ascii"))
+                    stats.report_receiver(results)
+
+                elif cmd == DISPLAY_RESULTS:
+                    s_ctrl.sendall(bytes([IPERF_DONE]))
+                    s_ctrl.close()
+                    time.sleep(1)  # delay so server is ready for any subsequent client connections
+                    return
+
+        stats.update()
+
+
+def main():
+    opt_mode = None
+    opt_udp = False
+    opt_reverse = False
+
+    sys.argv.pop(0)
+    while sys.argv:
+        opt = sys.argv.pop(0)
+        if opt == "-R":
+            opt_reverse = True
+        elif opt == "-u":
+            opt_udp = True
+        elif opt == "-s":
+            opt_mode = opt
+        elif opt == "-c":
+            opt_mode = opt
+            opt_host = sys.argv.pop(0)
+        else:
+            print("unknown option:", opt)
+            raise SystemExit(1)
+
+    if opt_mode == "-s":
+        server()
+    else:
+        client(opt_host, opt_udp, opt_reverse)
+
+
+if sys.platform == "linux":
+
+    def pollable_is_sock(pollable, sock):
+        return sock is not None and pollable[0] == sock.fileno()
+
+    def ticks_us():
+        return int(time.time() * 1e6)
+
+    def ticks_diff(a, b):
+        return a - b
+
+    if __name__ == "__main__":
+        main()
+else:
+
+    def pollable_is_sock(pollable, sock):
+        return pollable[0] == sock
+
+    from time import ticks_us, ticks_diff
diff --git a/python-ecosys/iperf3/manifest.py b/python-ecosys/iperf3/manifest.py
new file mode 100644
index 000000000..06964ce2a
--- /dev/null
+++ b/python-ecosys/iperf3/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.4", pypi="iperf3", pypi_publish="uiperf3")
+
+module("iperf3.py")
diff --git a/python-ecosys/pyjwt/jwt.py b/python-ecosys/pyjwt/jwt.py
new file mode 100644
index 000000000..11c28f479
--- /dev/null
+++ b/python-ecosys/pyjwt/jwt.py
@@ -0,0 +1,79 @@
+import binascii
+import hashlib
+import hmac
+import json
+from time import time
+
+
+def _to_b64url(data):
+    return (
+        binascii.b2a_base64(data)
+        .rstrip(b"\n")
+        .rstrip(b"=")
+        .replace(b"+", b"-")
+        .replace(b"/", b"_")
+    )
+
+
+def _from_b64url(data):
+    return binascii.a2b_base64(data.replace(b"-", b"+").replace(b"_", b"/") + b"===")
+
+
+class exceptions:
+    class PyJWTError(Exception):
+        pass
+
+    class InvalidTokenError(PyJWTError):
+        pass
+
+    class InvalidAlgorithmError(PyJWTError):
+        pass
+
+    class InvalidSignatureError(PyJWTError):
+        pass
+
+    class ExpiredSignatureError(PyJWTError):
+        pass
+
+
+def encode(payload, key, algorithm="HS256"):
+    if algorithm != "HS256":
+        raise exceptions.InvalidAlgorithmError
+
+    if isinstance(key, str):
+        key = key.encode()
+    header = _to_b64url(json.dumps({"typ": "JWT", "alg": algorithm}).encode())
+    payload = _to_b64url(json.dumps(payload).encode())
+    signature = _to_b64url(hmac.new(key, header + b"." + payload, hashlib.sha256).digest())
+    return (header + b"." + payload + b"." + signature).decode()
+
+
+def decode(token, key, algorithms=["HS256"]):
+    if "HS256" not in algorithms:
+        raise exceptions.InvalidAlgorithmError
+
+    parts = token.encode().split(b".")
+    if len(parts) != 3:
+        raise exceptions.InvalidTokenError
+
+    try:
+        header = json.loads(_from_b64url(parts[0]).decode())
+        payload = json.loads(_from_b64url(parts[1]).decode())
+        signature = _from_b64url(parts[2])
+    except Exception:
+        raise exceptions.InvalidTokenError
+
+    if header["alg"] not in algorithms or header["alg"] != "HS256":
+        raise exceptions.InvalidAlgorithmError
+
+    if isinstance(key, str):
+        key = key.encode()
+    calculated_signature = hmac.new(key, parts[0] + b"." + parts[1], hashlib.sha256).digest()
+    if signature != calculated_signature:
+        raise exceptions.InvalidSignatureError
+
+    if "exp" in payload:
+        if time() > payload["exp"]:
+            raise exceptions.ExpiredSignatureError
+
+    return payload
diff --git a/python-ecosys/pyjwt/manifest.py b/python-ecosys/pyjwt/manifest.py
new file mode 100644
index 000000000..b3de5efc9
--- /dev/null
+++ b/python-ecosys/pyjwt/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.1.0", pypi="pyjwt")
+
+require("hmac")
+
+module("jwt.py")
diff --git a/python-ecosys/pyjwt/test_jwt.py b/python-ecosys/pyjwt/test_jwt.py
new file mode 100644
index 000000000..fb30b8bbd
--- /dev/null
+++ b/python-ecosys/pyjwt/test_jwt.py
@@ -0,0 +1,28 @@
+import jwt
+from time import time
+
+secret_key = "top-secret!"
+
+token = jwt.encode({"user": "joe"}, secret_key, algorithm="HS256")
+print(token)
+decoded = jwt.decode(token, secret_key, algorithms=["HS256"])
+if decoded != {"user": "joe"}:
+    raise Exception("Invalid decoded JWT")
+else:
+    print("Encode/decode test: OK")
+
+try:
+    decoded = jwt.decode(token, "wrong-secret", algorithms=["HS256"])
+except jwt.exceptions.InvalidSignatureError:
+    print("Invalid signature test: OK")
+else:
+    raise Exception("Invalid JWT should have failed decoding")
+
+token = jwt.encode({"user": "joe", "exp": time() - 1}, secret_key)
+print(token)
+try:
+    decoded = jwt.decode(token, secret_key, algorithms=["HS256"])
+except jwt.exceptions.ExpiredSignatureError:
+    print("Expired token test: OK")
+else:
+    raise Exception("Expired JWT should have failed decoding")
diff --git a/python-ecosys/requests/README.md b/python-ecosys/requests/README.md
new file mode 100644
index 000000000..d6ceaadc5
--- /dev/null
+++ b/python-ecosys/requests/README.md
@@ -0,0 +1,16 @@
+## requests
+
+This module provides a lightweight version of the Python
+[requests](https://requests.readthedocs.io/en/latest/) library.
+
+It includes support for all HTTP verbs, https, json decoding of responses,
+redirects, basic authentication.
+
+### Limitations
+
+* Certificate validation is not currently supported.
+* A dictionary passed as post data will not do automatic JSON or
+  multipart-form encoding of post data (this can be done manually).
+* Compressed requests/responses are not currently supported.
+* File upload is not supported.
+* Chunked encoding in responses is not supported.
diff --git a/python-ecosys/requests/example_xively.py b/python-ecosys/requests/example_xively.py
new file mode 100644
index 000000000..60e139b98
--- /dev/null
+++ b/python-ecosys/requests/example_xively.py
@@ -0,0 +1,13 @@
+import requests
+
+r = requests.get("http://api.xively.com/")
+print(r)
+print(r.content)
+print(r.text)
+print(r.content)
+print(r.json())
+
+# It's mandatory to close response objects as soon as you finished
+# working with them. On MicroPython platforms without full-fledged
+# OS, not doing so may lead to resource leaks and malfunction.
+r.close()
diff --git a/python-ecosys/requests/manifest.py b/python-ecosys/requests/manifest.py
new file mode 100644
index 000000000..85f159753
--- /dev/null
+++ b/python-ecosys/requests/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.10.2", pypi="requests")
+
+package("requests")
diff --git a/python-ecosys/requests/requests/__init__.py b/python-ecosys/requests/requests/__init__.py
new file mode 100644
index 000000000..4ca7489a4
--- /dev/null
+++ b/python-ecosys/requests/requests/__init__.py
@@ -0,0 +1,221 @@
+import socket
+
+
+class Response:
+    def __init__(self, f):
+        self.raw = f
+        self.encoding = "utf-8"
+        self._cached = None
+
+    def close(self):
+        if self.raw:
+            self.raw.close()
+            self.raw = None
+        self._cached = None
+
+    @property
+    def content(self):
+        if self._cached is None:
+            try:
+                self._cached = self.raw.read()
+            finally:
+                self.raw.close()
+                self.raw = None
+        return self._cached
+
+    @property
+    def text(self):
+        return str(self.content, self.encoding)
+
+    def json(self):
+        import json
+
+        return json.loads(self.content)
+
+
+def request(
+    method,
+    url,
+    data=None,
+    json=None,
+    headers=None,
+    stream=None,
+    auth=None,
+    timeout=None,
+    parse_headers=True,
+):
+    if headers is None:
+        headers = {}
+    else:
+        headers = headers.copy()
+
+    redirect = None  # redirection url, None means no redirection
+    chunked_data = data and getattr(data, "__next__", None) and not getattr(data, "__len__", None)
+
+    if auth is not None:
+        import binascii
+
+        username, password = auth
+        formated = b"{}:{}".format(username, password)
+        formated = str(binascii.b2a_base64(formated)[:-1], "ascii")
+        headers["Authorization"] = "Basic {}".format(formated)
+
+    try:
+        proto, dummy, host, path = url.split("/", 3)
+    except ValueError:
+        proto, dummy, host = url.split("/", 2)
+        path = ""
+    if proto == "http:":
+        port = 80
+    elif proto == "https:":
+        import tls
+
+        port = 443
+    else:
+        raise ValueError("Unsupported protocol: " + proto)
+
+    if ":" in host:
+        host, port = host.split(":", 1)
+        port = int(port)
+
+    ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
+    ai = ai[0]
+
+    resp_d = None
+    if parse_headers is not False:
+        resp_d = {}
+
+    s = socket.socket(ai[0], socket.SOCK_STREAM, ai[2])
+
+    if timeout is not None:
+        # Note: settimeout is not supported on all platforms, will raise
+        # an AttributeError if not available.
+        s.settimeout(timeout)
+
+    try:
+        s.connect(ai[-1])
+        if proto == "https:":
+            context = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
+            context.verify_mode = tls.CERT_NONE
+            s = context.wrap_socket(s, server_hostname=host)
+        s.write(b"%s /%s HTTP/1.0\r\n" % (method, path))
+
+        if "Host" not in headers:
+            headers["Host"] = host
+
+        if json is not None:
+            assert data is None
+            from json import dumps
+
+            data = dumps(json)
+
+            if "Content-Type" not in headers:
+                headers["Content-Type"] = "application/json"
+
+        if data:
+            if chunked_data:
+                if "Transfer-Encoding" not in headers and "Content-Length" not in headers:
+                    headers["Transfer-Encoding"] = "chunked"
+            elif "Content-Length" not in headers:
+                headers["Content-Length"] = str(len(data))
+
+        if "Connection" not in headers:
+            headers["Connection"] = "close"
+
+        # Iterate over keys to avoid tuple alloc
+        for k in headers:
+            s.write(k)
+            s.write(b": ")
+            s.write(headers[k])
+            s.write(b"\r\n")
+
+        s.write(b"\r\n")
+
+        if data:
+            if chunked_data:
+                if headers.get("Transfer-Encoding", None) == "chunked":
+                    for chunk in data:
+                        s.write(b"%x\r\n" % len(chunk))
+                        s.write(chunk)
+                        s.write(b"\r\n")
+                    s.write("0\r\n\r\n")
+                else:
+                    for chunk in data:
+                        s.write(chunk)
+            else:
+                s.write(data)
+
+        l = s.readline()
+        # print(l)
+        l = l.split(None, 2)
+        if len(l) < 2:
+            # Invalid response
+            raise ValueError("HTTP error: BadStatusLine:\n%s" % l)
+        status = int(l[1])
+        reason = ""
+        if len(l) > 2:
+            reason = l[2].rstrip()
+        while True:
+            l = s.readline()
+            if not l or l == b"\r\n":
+                break
+            # print(l)
+            if l.startswith(b"Transfer-Encoding:"):
+                if b"chunked" in l:
+                    raise ValueError("Unsupported " + str(l, "utf-8"))
+            elif l.startswith(b"Location:") and not 200 <= status <= 299:
+                if status in [301, 302, 303, 307, 308]:
+                    redirect = str(l[10:-2], "utf-8")
+                else:
+                    raise NotImplementedError("Redirect %d not yet supported" % status)
+            if parse_headers is False:
+                pass
+            elif parse_headers is True:
+                l = str(l, "utf-8")
+                k, v = l.split(":", 1)
+                resp_d[k] = v.strip()
+            else:
+                parse_headers(l, resp_d)
+    except OSError:
+        s.close()
+        raise
+
+    if redirect:
+        s.close()
+        # Use the host specified in the redirect URL, as it may not be the same as the original URL.
+        headers.pop("Host", None)
+        if status in [301, 302, 303]:
+            return request("GET", redirect, None, None, headers, stream)
+        else:
+            return request(method, redirect, data, json, headers, stream)
+    else:
+        resp = Response(s)
+        resp.status_code = status
+        resp.reason = reason
+        if resp_d is not None:
+            resp.headers = resp_d
+        return resp
+
+
+def head(url, **kw):
+    return request("HEAD", url, **kw)
+
+
+def get(url, **kw):
+    return request("GET", url, **kw)
+
+
+def post(url, **kw):
+    return request("POST", url, **kw)
+
+
+def put(url, **kw):
+    return request("PUT", url, **kw)
+
+
+def patch(url, **kw):
+    return request("PATCH", url, **kw)
+
+
+def delete(url, **kw):
+    return request("DELETE", url, **kw)
diff --git a/python-ecosys/requests/test_requests.py b/python-ecosys/requests/test_requests.py
new file mode 100644
index 000000000..ac77291b0
--- /dev/null
+++ b/python-ecosys/requests/test_requests.py
@@ -0,0 +1,164 @@
+import io
+import sys
+
+
+class Socket:
+    def __init__(self):
+        self._write_buffer = io.BytesIO()
+        self._read_buffer = io.BytesIO(b"HTTP/1.0 200 OK\r\n\r\n")
+
+    def connect(self, address):
+        pass
+
+    def write(self, buf):
+        self._write_buffer.write(buf)
+
+    def readline(self):
+        return self._read_buffer.readline()
+
+
+class socket:
+    AF_INET = 2
+    SOCK_STREAM = 1
+    IPPROTO_TCP = 6
+
+    @staticmethod
+    def getaddrinfo(host, port, af=0, type=0, flags=0):
+        return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", ("127.0.0.1", 80))]
+
+    def socket(af=AF_INET, type=SOCK_STREAM, proto=IPPROTO_TCP):
+        return Socket()
+
+
+sys.modules["socket"] = socket
+# ruff: noqa: E402
+import requests
+
+
+def format_message(response):
+    return response.raw._write_buffer.getvalue().decode("utf8")
+
+
+def test_simple_get():
+    response = requests.request("GET", "http://example.com")
+
+    assert response.raw._write_buffer.getvalue() == (
+        b"GET / HTTP/1.0\r\n" + b"Connection: close\r\n" + b"Host: example.com\r\n\r\n"
+    ), format_message(response)
+
+
+def test_get_auth():
+    response = requests.request(
+        "GET", "http://example.com", auth=("test-username", "test-password")
+    )
+
+    assert response.raw._write_buffer.getvalue() == (
+        b"GET / HTTP/1.0\r\n"
+        + b"Host: example.com\r\n"
+        + b"Authorization: Basic dGVzdC11c2VybmFtZTp0ZXN0LXBhc3N3b3Jk\r\n"
+        + b"Connection: close\r\n\r\n"
+    ), format_message(response)
+
+
+def test_get_custom_header():
+    response = requests.request("GET", "http://example.com", headers={"User-Agent": "test-agent"})
+
+    assert response.raw._write_buffer.getvalue() == (
+        b"GET / HTTP/1.0\r\n"
+        + b"User-Agent: test-agent\r\n"
+        + b"Host: example.com\r\n"
+        + b"Connection: close\r\n\r\n"
+    ), format_message(response)
+
+
+def test_post_json():
+    response = requests.request("GET", "http://example.com", json="test")
+
+    assert response.raw._write_buffer.getvalue() == (
+        b"GET / HTTP/1.0\r\n"
+        + b"Connection: close\r\n"
+        + b"Content-Type: application/json\r\n"
+        + b"Host: example.com\r\n"
+        + b"Content-Length: 6\r\n\r\n"
+        + b'"test"'
+    ), format_message(response)
+
+
+def test_post_chunked_data():
+    def chunks():
+        yield "test"
+
+    response = requests.request("GET", "http://example.com", data=chunks())
+
+    assert response.raw._write_buffer.getvalue() == (
+        b"GET / HTTP/1.0\r\n"
+        + b"Transfer-Encoding: chunked\r\n"
+        + b"Host: example.com\r\n"
+        + b"Connection: close\r\n\r\n"
+        + b"4\r\ntest\r\n"
+        + b"0\r\n\r\n"
+    ), format_message(response)
+
+
+def test_overwrite_get_headers():
+    response = requests.request(
+        "GET", "http://example.com", headers={"Host": "test.com", "Connection": "keep-alive"}
+    )
+
+    assert response.raw._write_buffer.getvalue() == (
+        b"GET / HTTP/1.0\r\n" + b"Connection: keep-alive\r\n" + b"Host: test.com\r\n\r\n"
+    ), format_message(response)
+
+
+def test_overwrite_post_json_headers():
+    response = requests.request(
+        "GET",
+        "http://example.com",
+        json="test",
+        headers={"Content-Type": "text/plain", "Content-Length": "10"},
+    )
+
+    assert response.raw._write_buffer.getvalue() == (
+        b"GET / HTTP/1.0\r\n"
+        + b"Connection: close\r\n"
+        + b"Content-Length: 10\r\n"
+        + b"Content-Type: text/plain\r\n"
+        + b"Host: example.com\r\n\r\n"
+        + b'"test"'
+    ), format_message(response)
+
+
+def test_overwrite_post_chunked_data_headers():
+    def chunks():
+        yield "test"
+
+    response = requests.request(
+        "GET", "http://example.com", data=chunks(), headers={"Content-Length": "4"}
+    )
+
+    assert response.raw._write_buffer.getvalue() == (
+        b"GET / HTTP/1.0\r\n"
+        + b"Host: example.com\r\n"
+        + b"Content-Length: 4\r\n"
+        + b"Connection: close\r\n\r\n"
+        + b"test"
+    ), format_message(response)
+
+
+def test_do_not_modify_headers_argument():
+    global do_not_modify_this_dict
+    do_not_modify_this_dict = {}
+    requests.request("GET", "http://example.com", headers=do_not_modify_this_dict)
+
+    assert do_not_modify_this_dict == {}, do_not_modify_this_dict
+
+
+test_simple_get()
+test_get_auth()
+test_get_custom_header()
+test_post_json()
+test_post_chunked_data()
+test_overwrite_get_headers()
+test_overwrite_post_json_headers()
+test_overwrite_post_chunked_data_headers()
+test_do_not_modify_headers_argument()
diff --git a/python-stdlib/README.md b/python-stdlib/README.md
new file mode 100644
index 000000000..564776313
--- /dev/null
+++ b/python-stdlib/README.md
@@ -0,0 +1,18 @@
+## CPython Standard Library
+
+The packages in this directory aim to provide compatible implementations of
+modules from the Python Standard Library, with the goal of allowing existing
+Python code to run un-modified on MicroPython.
+
+### Implementation
+
+Many packages are implemented in pure Python, often based on the original
+CPython implementation. (e.g. `collections.defaultdict`)
+
+Some packages are based on or extend from the built-in "micro" modules in the
+MicroPython firmware, providing additional functionality that didn't need to
+be written in C (e.g. `collections`, `socket`, `struct`).
+
+### Future plans (ideas for contributors):
+
+* Add README.md to each library explaining compatibility and limitations.
diff --git a/__future__/__future__.py b/python-stdlib/__future__/__future__.py
similarity index 88%
rename from __future__/__future__.py
rename to python-stdlib/__future__/__future__.py
index 45b935edc..178294c96 100644
--- a/__future__/__future__.py
+++ b/python-stdlib/__future__/__future__.py
@@ -5,3 +5,4 @@
 with_statement = True
 print_function = True
 unicode_literals = True
+annotations = True
diff --git a/python-stdlib/__future__/manifest.py b/python-stdlib/__future__/manifest.py
new file mode 100644
index 000000000..e06f3268d
--- /dev/null
+++ b/python-stdlib/__future__/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.0")
+
+module("__future__.py")
diff --git a/python-stdlib/abc/abc.py b/python-stdlib/abc/abc.py
new file mode 100644
index 000000000..c2c707f62
--- /dev/null
+++ b/python-stdlib/abc/abc.py
@@ -0,0 +1,6 @@
+class ABC:
+    pass
+
+
+def abstractmethod(f):
+    return f
diff --git a/python-stdlib/abc/manifest.py b/python-stdlib/abc/manifest.py
new file mode 100644
index 000000000..c76312960
--- /dev/null
+++ b/python-stdlib/abc/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.0")
+
+module("abc.py")
diff --git a/argparse/argparse.py b/python-stdlib/argparse/argparse.py
similarity index 78%
rename from argparse/argparse.py
rename to python-stdlib/argparse/argparse.py
index 2e06857a0..5c92887f9 100644
--- a/argparse/argparse.py
+++ b/python-stdlib/argparse/argparse.py
@@ -3,7 +3,7 @@
 """
 
 import sys
-from _collections import namedtuple
+from collections import namedtuple
 
 
 class _ArgError(BaseException):
@@ -104,8 +104,16 @@ def add_argument(self, *args, **kwargs):
             if not args:
                 args = [dest]
         list.append(
-            _Arg(args, dest, action, kwargs.get("nargs", None),
-                 const, default, kwargs.get("help", "")))
+            _Arg(
+                args,
+                dest,
+                action,
+                kwargs.get("nargs", None),
+                const,
+                default,
+                kwargs.get("help", ""),
+            )
+        )
 
     def usage(self, full):
         # print short usage
@@ -121,8 +129,9 @@ def render_arg(arg):
                     return " %s%s" % (arg.dest, arg.nargs)
             else:
                 return ""
+
         for opt in self.opt:
-            print(" [%s%s]" % (', '.join(opt.names), render_arg(opt)), end="")
+            print(" [%s%s]" % (", ".join(opt.names), render_arg(opt)), end="")
         for pos in self.pos:
             print(render_arg(pos), end="")
         print()
@@ -141,21 +150,27 @@ def render_arg(arg):
         print("\noptional args:")
         print("  -h, --help      show this message and exit")
         for opt in self.opt:
-            print("  %-16s%s" % (', '.join(opt.names) + render_arg(opt), opt.help))
+            print("  %-16s%s" % (", ".join(opt.names) + render_arg(opt), opt.help))
 
     def parse_args(self, args=None):
+        return self._parse_args_impl(args, False)
+
+    def parse_known_args(self, args=None):
+        return self._parse_args_impl(args, True)
+
+    def _parse_args_impl(self, args, return_unknown):
         if args is None:
             args = sys.argv[1:]
         else:
             args = args[:]
         try:
-            return self._parse_args(args)
+            return self._parse_args(args, return_unknown)
         except _ArgError as e:
             self.usage(False)
             print("error:", e)
             sys.exit(2)
 
-    def _parse_args(self, args):
+    def _parse_args(self, args, return_unknown):
         # add optional args with defaults
         arg_dest = []
         arg_vals = []
@@ -163,6 +178,13 @@ def _parse_args(self, args):
             arg_dest.append(opt.dest)
             arg_vals.append(opt.default)
 
+        # deal with unknown arguments, if needed
+        unknown = []
+
+        def consume_unknown():
+            while args and not args[0].startswith("-"):
+                unknown.append(args.pop(0))
+
         # parse all args
         parsed_pos = False
         while args or not parsed_pos:
@@ -179,15 +201,26 @@ def _parse_args(self, args):
                         found = True
                         break
                 if not found:
-                    raise _ArgError("unknown option %s" % a)
+                    if return_unknown:
+                        unknown.append(a)
+                        consume_unknown()
+                    else:
+                        raise _ArgError("unknown option %s" % a)
             else:
                 # positional arg
                 if parsed_pos:
-                    raise _ArgError("extra args: %s" % " ".join(args))
+                    if return_unknown:
+                        unknown = unknown + args
+                        break
+                    else:
+                        raise _ArgError("extra args: %s" % " ".join(args))
                 for pos in self.pos:
                     arg_dest.append(pos.dest)
                     arg_vals.append(pos.parse(pos.names[0], args))
                 parsed_pos = True
+                if return_unknown:
+                    consume_unknown()
 
         # build and return named tuple with arg values
-        return namedtuple("args", arg_dest)(*arg_vals)
+        values = namedtuple("args", arg_dest)(*arg_vals)
+        return (values, unknown) if return_unknown else values
diff --git a/python-stdlib/argparse/manifest.py b/python-stdlib/argparse/manifest.py
new file mode 100644
index 000000000..02bf1a22c
--- /dev/null
+++ b/python-stdlib/argparse/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.4.0")
+
+# Originally written by Damien George.
+
+module("argparse.py")
diff --git a/python-stdlib/argparse/test_argparse.py b/python-stdlib/argparse/test_argparse.py
new file mode 100644
index 000000000..d86e53211
--- /dev/null
+++ b/python-stdlib/argparse/test_argparse.py
@@ -0,0 +1,68 @@
+import argparse
+
+parser = argparse.ArgumentParser(description="command line program")
+parser.add_argument("a")
+parser.add_argument("b")
+parser.add_argument(dest="c")
+args = parser.parse_args(["1", "2", "3"])
+assert args.a == "1" and args.b == "2" and args.c == "3"
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-a", action="store_true")
+parser.add_argument("-b", default=123)
+args = parser.parse_args([])
+assert args.a is False and args.b == 123
+args = parser.parse_args(["-a"])
+assert args.a is True and args.b == 123
+args = parser.parse_args(["-b", "456"])
+assert args.a is False and args.b == "456"
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-a", "--a-opt", action="store_true")
+parser.add_argument("-b", "--b-opt", default=123)
+parser.add_argument("--c-opt", default="test")
+args = parser.parse_args([])
+assert args.a_opt is False and args.b_opt == 123 and args.c_opt == "test"
+args = parser.parse_args(["--a-opt"])
+assert args.a_opt is True and args.b_opt == 123 and args.c_opt == "test"
+args = parser.parse_args(["--b-opt", "456"])
+assert args.a_opt is False and args.b_opt == "456" and args.c_opt == "test"
+args = parser.parse_args(["--c-opt", "override"])
+assert args.a_opt is False and args.b_opt == 123 and args.c_opt == "override"
+
+parser = argparse.ArgumentParser()
+parser.add_argument("files", nargs="+")
+args = parser.parse_args(["a"])
+assert args.files == ["a"]
+args = parser.parse_args(["a", "b", "c"])
+assert args.files == ["a", "b", "c"]
+
+parser = argparse.ArgumentParser()
+parser.add_argument("files1", nargs=2)
+parser.add_argument("files2", nargs="*")
+args = parser.parse_args(["a", "b"])
+assert args.files1 == ["a", "b"] and args.files2 == []
+args = parser.parse_args(["a", "b", "c"])
+assert args.files1 == ["a", "b"] and args.files2 == ["c"]
+
+parser = argparse.ArgumentParser()
+parser.add_argument("a", nargs=2)
+parser.add_argument("-b")
+args, rest = parser.parse_known_args(["a", "b", "-b", "2"])
+assert args.a == ["a", "b"] and args.b == "2"
+assert rest == []
+args, rest = parser.parse_known_args(["-b", "2", "a", "b", "c"])
+assert args.a == ["a", "b"] and args.b == "2"
+assert rest == ["c"]
+args, rest = parser.parse_known_args(["a", "b", "-b", "2", "c"])
+assert args.a == ["a", "b"] and args.b == "2"
+assert rest == ["c"]
+args, rest = parser.parse_known_args(["-b", "2", "a", "b", "-", "c"])
+assert args.a == ["a", "b"] and args.b == "2"
+assert rest == ["-", "c"]
+args, rest = parser.parse_known_args(["a", "b", "-b", "2", "-", "x", "y"])
+assert args.a == ["a", "b"] and args.b == "2"
+assert rest == ["-", "x", "y"]
+args, rest = parser.parse_known_args(["a", "b", "c", "-b", "2", "--x", "5", "1"])
+assert args.a == ["a", "b"] and args.b == "2"
+assert rest == ["c", "--x", "5", "1"]
diff --git a/python-stdlib/base64/base64.py b/python-stdlib/base64/base64.py
new file mode 100644
index 000000000..d6baca05f
--- /dev/null
+++ b/python-stdlib/base64/base64.py
@@ -0,0 +1,480 @@
+#! /usr/bin/env python3
+
+"""RFC 3548: Base16, Base32, Base64 Data Encodings"""
+
+# Modified 04-Oct-1995 by Jack Jansen to use binascii module
+# Modified 30-Dec-2003 by Barry Warsaw to add full RFC 3548 support
+# Modified 22-May-2007 by Guido van Rossum to use bytes everywhere
+
+import re
+import struct
+import binascii
+
+
+__all__ = [
+    # Legacy interface exports traditional RFC 1521 Base64 encodings
+    "encode",
+    "decode",
+    "encodebytes",
+    "decodebytes",
+    # Generalized interface for other encodings
+    "b64encode",
+    "b64decode",
+    "b32encode",
+    "b32decode",
+    "b16encode",
+    "b16decode",
+    # Standard Base64 encoding
+    "standard_b64encode",
+    "standard_b64decode",
+    # Some common Base64 alternatives.  As referenced by RFC 3458, see thread
+    # starting at:
+    #
+    # http://zgp.org/pipermail/p2p-hackers/2001-September/000316.html
+    "urlsafe_b64encode",
+    "urlsafe_b64decode",
+]
+
+
+bytes_types = (bytes, bytearray)  # Types acceptable as binary data
+
+
+def _bytes_from_decode_data(s):
+    if isinstance(s, str):
+        try:
+            return s.encode("ascii")
+        #        except UnicodeEncodeError:
+        except:
+            raise ValueError("string argument should contain only ASCII characters")
+    elif isinstance(s, bytes_types):
+        return s
+    else:
+        raise TypeError("argument should be bytes or ASCII string, not %s" % s.__class__.__name__)
+
+
+def _maketrans(f, t):
+    """Re-implement bytes.maketrans() as there is no such function in micropython"""
+    if len(f) != len(t):
+        raise ValueError("maketrans arguments must have same length")
+    translation_table = dict(zip(f, t))
+    return translation_table
+
+
+def _translate(input_bytes, trans_table):
+    """Re-implement bytes.translate() as there is no such function in micropython"""
+    result = bytearray()
+
+    for byte in input_bytes:
+        translated_byte = trans_table.get(byte, byte)
+        result.append(translated_byte)
+
+    return bytes(result)
+
+
+# Base64 encoding/decoding uses binascii
+
+
+def b64encode(s, altchars=None):
+    """Encode a byte string using Base64.
+
+    s is the byte string to encode.  Optional altchars must be a byte
+    string of length 2 which specifies an alternative alphabet for the
+    '+' and '/' characters.  This allows an application to
+    e.g. generate url or filesystem safe Base64 strings.
+
+    The encoded byte string is returned.
+    """
+    if not isinstance(s, bytes_types):
+        raise TypeError("expected bytes, not %s" % s.__class__.__name__)
+    # Strip off the trailing newline
+    encoded = binascii.b2a_base64(s)[:-1]
+    if altchars is not None:
+        if not isinstance(altchars, bytes_types):
+            raise TypeError("expected bytes, not %s" % altchars.__class__.__name__)
+        assert len(altchars) == 2, repr(altchars)
+        encoded = _translate(encoded, _maketrans(b"+/", altchars))
+    return encoded
+
+
+def b64decode(s, altchars=None, validate=False):
+    """Decode a Base64 encoded byte string.
+
+    s is the byte string to decode.  Optional altchars must be a
+    string of length 2 which specifies the alternative alphabet used
+    instead of the '+' and '/' characters.
+
+    The decoded string is returned.  A binascii.Error is raised if s is
+    incorrectly padded.
+
+    If validate is False (the default), non-base64-alphabet characters are
+    discarded prior to the padding check.  If validate is True,
+    non-base64-alphabet characters in the input result in a binascii.Error.
+    """
+    s = _bytes_from_decode_data(s)
+    if altchars is not None:
+        altchars = _bytes_from_decode_data(altchars)
+        assert len(altchars) == 2, repr(altchars)
+        s = _translate(s, _maketrans(altchars, b"+/"))
+    if validate and not re.match(b"^[A-Za-z0-9+/]*=*$", s):
+        raise binascii.Error("Non-base64 digit found")
+    return binascii.a2b_base64(s)
+
+
+def standard_b64encode(s):
+    """Encode a byte string using the standard Base64 alphabet.
+
+    s is the byte string to encode.  The encoded byte string is returned.
+    """
+    return b64encode(s)
+
+
+def standard_b64decode(s):
+    """Decode a byte string encoded with the standard Base64 alphabet.
+
+    s is the byte string to decode.  The decoded byte string is
+    returned.  binascii.Error is raised if the input is incorrectly
+    padded or if there are non-alphabet characters present in the
+    input.
+    """
+    return b64decode(s)
+
+
+# _urlsafe_encode_translation = _maketrans(b'+/', b'-_')
+# _urlsafe_decode_translation = _maketrans(b'-_', b'+/')
+
+
+def urlsafe_b64encode(s):
+    """Encode a byte string using a url-safe Base64 alphabet.
+
+    s is the byte string to encode.  The encoded byte string is
+    returned.  The alphabet uses '-' instead of '+' and '_' instead of
+    '/'.
+    """
+    #    return b64encode(s).translate(_urlsafe_encode_translation)
+    return b64encode(s, b"-_").rstrip(b"\n")
+
+
+def urlsafe_b64decode(s):
+    """Decode a byte string encoded with the standard Base64 alphabet.
+
+    s is the byte string to decode.  The decoded byte string is
+    returned.  binascii.Error is raised if the input is incorrectly
+    padded or if there are non-alphabet characters present in the
+    input.
+
+    The alphabet uses '-' instead of '+' and '_' instead of '/'.
+    """
+    #    s = _bytes_from_decode_data(s)
+    #    s = s.translate(_urlsafe_decode_translation)
+    #    return b64decode(s)
+    raise NotImplementedError()
+
+
+# Base32 encoding/decoding must be done in Python
+_b32alphabet = {
+    0: b"A",
+    9: b"J",
+    18: b"S",
+    27: b"3",
+    1: b"B",
+    10: b"K",
+    19: b"T",
+    28: b"4",
+    2: b"C",
+    11: b"L",
+    20: b"U",
+    29: b"5",
+    3: b"D",
+    12: b"M",
+    21: b"V",
+    30: b"6",
+    4: b"E",
+    13: b"N",
+    22: b"W",
+    31: b"7",
+    5: b"F",
+    14: b"O",
+    23: b"X",
+    6: b"G",
+    15: b"P",
+    24: b"Y",
+    7: b"H",
+    16: b"Q",
+    25: b"Z",
+    8: b"I",
+    17: b"R",
+    26: b"2",
+}
+
+_b32tab = [v[0] for k, v in sorted(_b32alphabet.items())]
+_b32rev = dict([(v[0], k) for k, v in _b32alphabet.items()])
+
+
+def b32encode(s):
+    """Encode a byte string using Base32.
+
+    s is the byte string to encode.  The encoded byte string is returned.
+    """
+    if not isinstance(s, bytes_types):
+        raise TypeError("expected bytes, not %s" % s.__class__.__name__)
+    quanta, leftover = divmod(len(s), 5)
+    # Pad the last quantum with zero bits if necessary
+    if leftover:
+        s = s + bytes(5 - leftover)  # Don't use += !
+        quanta += 1
+    encoded = bytearray()
+    for i in range(quanta):
+        # c1 and c2 are 16 bits wide, c3 is 8 bits wide.  The intent of this
+        # code is to process the 40 bits in units of 5 bits.  So we take the 1
+        # leftover bit of c1 and tack it onto c2.  Then we take the 2 leftover
+        # bits of c2 and tack them onto c3.  The shifts and masks are intended
+        # to give us values of exactly 5 bits in width.
+        c1, c2, c3 = struct.unpack("!HHB", s[i * 5 : (i + 1) * 5])
+        c2 += (c1 & 1) << 16  # 17 bits wide
+        c3 += (c2 & 3) << 8  # 10 bits wide
+        encoded += bytes(
+            [
+                _b32tab[c1 >> 11],  # bits 1 - 5
+                _b32tab[(c1 >> 6) & 0x1F],  # bits 6 - 10
+                _b32tab[(c1 >> 1) & 0x1F],  # bits 11 - 15
+                _b32tab[c2 >> 12],  # bits 16 - 20 (1 - 5)
+                _b32tab[(c2 >> 7) & 0x1F],  # bits 21 - 25 (6 - 10)
+                _b32tab[(c2 >> 2) & 0x1F],  # bits 26 - 30 (11 - 15)
+                _b32tab[c3 >> 5],  # bits 31 - 35 (1 - 5)
+                _b32tab[c3 & 0x1F],  # bits 36 - 40 (1 - 5)
+            ]
+        )
+    # Adjust for any leftover partial quanta
+    if leftover == 1:
+        encoded = encoded[:-6] + b"======"
+    elif leftover == 2:
+        encoded = encoded[:-4] + b"===="
+    elif leftover == 3:
+        encoded = encoded[:-3] + b"==="
+    elif leftover == 4:
+        encoded = encoded[:-1] + b"="
+    return bytes(encoded)
+
+
+def b32decode(s, casefold=False, map01=None):
+    """Decode a Base32 encoded byte string.
+
+    s is the byte string to decode.  Optional casefold is a flag
+    specifying whether a lowercase alphabet is acceptable as input.
+    For security purposes, the default is False.
+
+    RFC 3548 allows for optional mapping of the digit 0 (zero) to the
+    letter O (oh), and for optional mapping of the digit 1 (one) to
+    either the letter I (eye) or letter L (el).  The optional argument
+    map01 when not None, specifies which letter the digit 1 should be
+    mapped to (when map01 is not None, the digit 0 is always mapped to
+    the letter O).  For security purposes the default is None, so that
+    0 and 1 are not allowed in the input.
+
+    The decoded byte string is returned.  binascii.Error is raised if
+    the input is incorrectly padded or if there are non-alphabet
+    characters present in the input.
+    """
+    s = _bytes_from_decode_data(s)
+    quanta, leftover = divmod(len(s), 8)
+    if leftover:
+        raise binascii.Error("Incorrect padding")
+    # Handle section 2.4 zero and one mapping.  The flag map01 will be either
+    # False, or the character to map the digit 1 (one) to.  It should be
+    # either L (el) or I (eye).
+    if map01 is not None:
+        map01 = _bytes_from_decode_data(map01)
+        assert len(map01) == 1, repr(map01)
+        s = _translate(s, _maketrans(b"01", b"O" + map01))
+    if casefold:
+        s = s.upper()
+    # Strip off pad characters from the right.  We need to count the pad
+    # characters because this will tell us how many null bytes to remove from
+    # the end of the decoded string.
+    padchars = s.find(b"=")
+    if padchars > 0:
+        padchars = len(s) - padchars
+        s = s[:-padchars]
+    else:
+        padchars = 0
+
+    # Now decode the full quanta
+    parts = []
+    acc = 0
+    shift = 35
+    for c in s:
+        val = _b32rev.get(c)
+        if val is None:
+            raise binascii.Error("Non-base32 digit found")
+        acc += _b32rev[c] << shift
+        shift -= 5
+        if shift < 0:
+            parts.append(binascii.unhexlify(bytes("%010x" % acc, "ascii")))
+            acc = 0
+            shift = 35
+    # Process the last, partial quanta
+    last = binascii.unhexlify(bytes("%010x" % acc, "ascii"))
+    if padchars == 0:
+        last = b""  # No characters
+    elif padchars == 1:
+        last = last[:-1]
+    elif padchars == 3:
+        last = last[:-2]
+    elif padchars == 4:
+        last = last[:-3]
+    elif padchars == 6:
+        last = last[:-4]
+    else:
+        raise binascii.Error("Incorrect padding")
+    parts.append(last)
+    return b"".join(parts)
+
+
+# RFC 3548, Base 16 Alphabet specifies uppercase, but hexlify() returns
+# lowercase.  The RFC also recommends against accepting input case
+# insensitively.
+def b16encode(s):
+    """Encode a byte string using Base16.
+
+    s is the byte string to encode.  The encoded byte string is returned.
+    """
+    if not isinstance(s, bytes_types):
+        raise TypeError("expected bytes, not %s" % s.__class__.__name__)
+    return binascii.hexlify(s).upper()
+
+
+def b16decode(s, casefold=False):
+    """Decode a Base16 encoded byte string.
+
+    s is the byte string to decode.  Optional casefold is a flag
+    specifying whether a lowercase alphabet is acceptable as input.
+    For security purposes, the default is False.
+
+    The decoded byte string is returned.  binascii.Error is raised if
+    s were incorrectly padded or if there are non-alphabet characters
+    present in the string.
+    """
+    s = _bytes_from_decode_data(s)
+    if casefold:
+        s = s.upper()
+    if re.search(b"[^0-9A-F]", s):
+        raise binascii.Error("Non-base16 digit found")
+    return binascii.unhexlify(s)
+
+
+# Legacy interface.  This code could be cleaned up since I don't believe
+# binascii has any line length limitations.  It just doesn't seem worth it
+# though.  The files should be opened in binary mode.
+
+MAXLINESIZE = 76  # Excluding the CRLF
+MAXBINSIZE = (MAXLINESIZE // 4) * 3
+
+
+def encode(input, output):
+    """Encode a file; input and output are binary files."""
+    while True:
+        s = input.read(MAXBINSIZE)
+        if not s:
+            break
+        while len(s) < MAXBINSIZE:
+            ns = input.read(MAXBINSIZE - len(s))
+            if not ns:
+                break
+            s += ns
+        line = binascii.b2a_base64(s)
+        output.write(line)
+
+
+def decode(input, output):
+    """Decode a file; input and output are binary files."""
+    while True:
+        line = input.readline()
+        if not line:
+            break
+        s = binascii.a2b_base64(line)
+        output.write(s)
+
+
+def encodebytes(s):
+    """Encode a bytestring into a bytestring containing multiple lines
+    of base-64 data."""
+    if not isinstance(s, bytes_types):
+        raise TypeError("expected bytes, not %s" % s.__class__.__name__)
+    pieces = []
+    for i in range(0, len(s), MAXBINSIZE):
+        chunk = s[i : i + MAXBINSIZE]
+        pieces.append(binascii.b2a_base64(chunk))
+    return b"".join(pieces)
+
+
+def encodestring(s):
+    """Legacy alias of encodebytes()."""
+    import warnings
+
+    warnings.warn("encodestring() is a deprecated alias, use encodebytes()", DeprecationWarning, 2)
+    return encodebytes(s)
+
+
+def decodebytes(s):
+    """Decode a bytestring of base-64 data into a bytestring."""
+    if not isinstance(s, bytes_types):
+        raise TypeError("expected bytes, not %s" % s.__class__.__name__)
+    return binascii.a2b_base64(s)
+
+
+def decodestring(s):
+    """Legacy alias of decodebytes()."""
+    import warnings
+
+    warnings.warn("decodestring() is a deprecated alias, use decodebytes()", DeprecationWarning, 2)
+    return decodebytes(s)
+
+
+# Usable as a script...
+def main():
+    """Small main program"""
+    import sys, getopt
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "deut")
+    except getopt.error as msg:
+        sys.stdout = sys.stderr
+        print(msg)
+        print(
+            """usage: %s [-d|-e|-u|-t] [file|-]
+        -d, -u: decode
+        -e: encode (default)
+        -t: encode and decode string 'Aladdin:open sesame'"""
+            % sys.argv[0]
+        )
+        sys.exit(2)
+    func = encode
+    for o, a in opts:
+        if o == "-e":
+            func = encode
+        if o == "-d":
+            func = decode
+        if o == "-u":
+            func = decode
+        if o == "-t":
+            test()
+            return
+    if args and args[0] != "-":
+        with open(args[0], "rb") as f:
+            func(f, sys.stdout.buffer)
+    else:
+        func(sys.stdin.buffer, sys.stdout.buffer)
+
+
+def test():
+    s0 = b"Aladdin:open sesame"
+    print(repr(s0))
+    s1 = encodebytes(s0)
+    print(repr(s1))
+    s2 = decodebytes(s1)
+    print(repr(s2))
+    assert s0 == s2
+
+
+if __name__ == "__main__":
+    main()
diff --git a/python-stdlib/base64/manifest.py b/python-stdlib/base64/manifest.py
new file mode 100644
index 000000000..9e1b31751
--- /dev/null
+++ b/python-stdlib/base64/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="3.3.6")
+
+require("binascii")
+
+module("base64.py")
diff --git a/python-stdlib/base64/test_base64.py b/python-stdlib/base64/test_base64.py
new file mode 100644
index 000000000..b29c29c84
--- /dev/null
+++ b/python-stdlib/base64/test_base64.py
@@ -0,0 +1,36 @@
+import base64
+
+b = base64.b64encode(b"zlutoucky kun upel dabelske ody")
+print(b)
+
+if b != b"emx1dG91Y2t5IGt1biB1cGVsIGRhYmVsc2tlIG9keQ==":
+    raise Exception("Error")
+
+d = base64.b64decode(b)
+print(d)
+
+if d != b"zlutoucky kun upel dabelske ody":
+    raise Exception("Error")
+
+base64.test()
+
+binary = b"\x99\x10\xaa"
+b = base64.b64encode(binary)
+if b != b"mRCq":
+    raise Exception("Error")
+
+d = base64.b64decode(b)
+print(d)
+if d != binary:
+    raise Exception("Error")
+
+d = base64.b32encode(b"zlutoucky kun upel dabelske ody")
+if d != b"PJWHK5DPOVRWW6JANN2W4IDVOBSWYIDEMFRGK3DTNNSSA33EPE======":
+    raise Exception("Error")
+
+print(d)
+b = base64.b32decode(d)
+if b != b"zlutoucky kun upel dabelske ody":
+    raise Exception("Error")
+
+print("OK")
diff --git a/python-stdlib/binascii/binascii.py b/python-stdlib/binascii/binascii.py
new file mode 100644
index 000000000..f2ec39e84
--- /dev/null
+++ b/python-stdlib/binascii/binascii.py
@@ -0,0 +1,362 @@
+from ubinascii import *
+
+if not "unhexlify" in globals():
+
+    def unhexlify(data):
+        if len(data) % 2 != 0:
+            raise ValueError("Odd-length string")
+
+        return bytes([int(data[i : i + 2], 16) for i in range(0, len(data), 2)])
+
+
+b2a_hex = hexlify
+a2b_hex = unhexlify
+
+# ____________________________________________________________
+
+PAD = "="
+
+table_a2b_base64 = [
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    62,
+    -1,
+    -1,
+    -1,
+    63,
+    52,
+    53,
+    54,
+    55,
+    56,
+    57,
+    58,
+    59,
+    60,
+    61,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,  # Note PAD->-1 here
+    -1,
+    0,
+    1,
+    2,
+    3,
+    4,
+    5,
+    6,
+    7,
+    8,
+    9,
+    10,
+    11,
+    12,
+    13,
+    14,
+    15,
+    16,
+    17,
+    18,
+    19,
+    20,
+    21,
+    22,
+    23,
+    24,
+    25,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    26,
+    27,
+    28,
+    29,
+    30,
+    31,
+    32,
+    33,
+    34,
+    35,
+    36,
+    37,
+    38,
+    39,
+    40,
+    41,
+    42,
+    43,
+    44,
+    45,
+    46,
+    47,
+    48,
+    49,
+    50,
+    51,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+    -1,
+]
+
+
+def _transform(n):
+    if n == -1:
+        return "\xff"
+    else:
+        return chr(n)
+
+
+table_a2b_base64 = "".join(map(_transform, table_a2b_base64))
+assert len(table_a2b_base64) == 256
+
+
+def a2b_base64(ascii):
+    "Decode a line of base64 data."
+
+    res = []
+    quad_pos = 0
+    leftchar = 0
+    leftbits = 0
+    last_char_was_a_pad = False
+
+    for c in ascii:
+        c = chr(c)
+        if c == PAD:
+            if quad_pos > 2 or (quad_pos == 2 and last_char_was_a_pad):
+                break  # stop on 'xxx=' or on 'xx=='
+            last_char_was_a_pad = True
+        else:
+            n = ord(table_a2b_base64[ord(c)])
+            if n == 0xFF:
+                continue  # ignore strange characters
+            #
+            # Shift it in on the low end, and see if there's
+            # a byte ready for output.
+            quad_pos = (quad_pos + 1) & 3
+            leftchar = (leftchar << 6) | n
+            leftbits += 6
+            #
+            if leftbits >= 8:
+                leftbits -= 8
+                res.append((leftchar >> leftbits).to_bytes(1, "big"))
+                leftchar &= (1 << leftbits) - 1
+            #
+            last_char_was_a_pad = False
+    else:
+        if leftbits != 0:
+            raise Exception("Incorrect padding")
+
+    return b"".join(res)
+
+
+# ____________________________________________________________
+
+table_b2a_base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+
+
+def b2a_base64(bin, newline=True):
+    "Base64-code line of data."
+
+    newlength = (len(bin) + 2) // 3
+    newlength = newlength * 4 + 1
+    res = []
+
+    leftchar = 0
+    leftbits = 0
+    for c in bin:
+        # Shift into our buffer, and output any 6bits ready
+        leftchar = (leftchar << 8) | c
+        leftbits += 8
+        res.append(table_b2a_base64[(leftchar >> (leftbits - 6)) & 0x3F])
+        leftbits -= 6
+        if leftbits >= 6:
+            res.append(table_b2a_base64[(leftchar >> (leftbits - 6)) & 0x3F])
+            leftbits -= 6
+    #
+    if leftbits == 2:
+        res.append(table_b2a_base64[(leftchar & 3) << 4])
+        res.append(PAD)
+        res.append(PAD)
+    elif leftbits == 4:
+        res.append(table_b2a_base64[(leftchar & 0xF) << 2])
+        res.append(PAD)
+    if newline:
+        res.append("\n")
+    return "".join(res).encode("ascii")
diff --git a/python-stdlib/binascii/manifest.py b/python-stdlib/binascii/manifest.py
new file mode 100644
index 000000000..c637678a1
--- /dev/null
+++ b/python-stdlib/binascii/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="2.4.1")
+
+module("binascii.py")
diff --git a/python-stdlib/binascii/test_binascii.py b/python-stdlib/binascii/test_binascii.py
new file mode 100644
index 000000000..075b2ff3c
--- /dev/null
+++ b/python-stdlib/binascii/test_binascii.py
@@ -0,0 +1,23 @@
+from binascii import *
+import time
+
+data = b"zlutoucky kun upel dabelske ody"
+h = hexlify(data)
+
+if h != b"7a6c75746f75636b79206b756e207570656c20646162656c736b65206f6479":
+    raise Exception("Error")
+
+data2 = unhexlify(h)
+
+if data2 != data:
+    raise Exception("Error")
+
+a2b_base64(b"as==") == b"j"
+
+start = time.time()
+for x in range(100000):
+    d = unhexlify(h)
+
+print("100000 iterations in: " + str(time.time() - start))
+
+print("OK")
diff --git a/python-stdlib/bisect/bisect.py b/python-stdlib/bisect/bisect.py
new file mode 100644
index 000000000..3fb29cc04
--- /dev/null
+++ b/python-stdlib/bisect/bisect.py
@@ -0,0 +1,106 @@
+"""Bisection algorithms."""
+
+
+def insort_right(a, x, lo=0, hi=None):
+    """Insert item x in list a, and keep it sorted assuming a is sorted.
+
+    If x is already in a, insert it to the right of the rightmost x.
+
+    Optional args lo (default 0) and hi (default len(a)) bound the
+    slice of a to be searched.
+    """
+
+    if lo < 0:
+        raise ValueError("lo must be non-negative")
+    if hi is None:
+        hi = len(a)
+    while lo < hi:
+        mid = (lo + hi) // 2
+        if x < a[mid]:
+            hi = mid
+        else:
+            lo = mid + 1
+    a.insert(lo, x)
+
+
+insort = insort_right  # backward compatibility
+
+
+def bisect_right(a, x, lo=0, hi=None):
+    """Return the index where to insert item x in list a, assuming a is sorted.
+
+    The return value i is such that all e in a[:i] have e <= x, and all e in
+    a[i:] have e > x.  So if x already appears in the list, a.insert(x) will
+    insert just after the rightmost x already there.
+
+    Optional args lo (default 0) and hi (default len(a)) bound the
+    slice of a to be searched.
+    """
+
+    if lo < 0:
+        raise ValueError("lo must be non-negative")
+    if hi is None:
+        hi = len(a)
+    while lo < hi:
+        mid = (lo + hi) // 2
+        if x < a[mid]:
+            hi = mid
+        else:
+            lo = mid + 1
+    return lo
+
+
+bisect = bisect_right  # backward compatibility
+
+
+def insort_left(a, x, lo=0, hi=None):
+    """Insert item x in list a, and keep it sorted assuming a is sorted.
+
+    If x is already in a, insert it to the left of the leftmost x.
+
+    Optional args lo (default 0) and hi (default len(a)) bound the
+    slice of a to be searched.
+    """
+
+    if lo < 0:
+        raise ValueError("lo must be non-negative")
+    if hi is None:
+        hi = len(a)
+    while lo < hi:
+        mid = (lo + hi) // 2
+        if a[mid] < x:
+            lo = mid + 1
+        else:
+            hi = mid
+    a.insert(lo, x)
+
+
+def bisect_left(a, x, lo=0, hi=None):
+    """Return the index where to insert item x in list a, assuming a is sorted.
+
+    The return value i is such that all e in a[:i] have e < x, and all e in
+    a[i:] have e >= x.  So if x already appears in the list, a.insert(x) will
+    insert just before the leftmost x already there.
+
+    Optional args lo (default 0) and hi (default len(a)) bound the
+    slice of a to be searched.
+    """
+
+    if lo < 0:
+        raise ValueError("lo must be non-negative")
+    if hi is None:
+        hi = len(a)
+    while lo < hi:
+        mid = (lo + hi) // 2
+        if a[mid] < x:
+            lo = mid + 1
+        else:
+            hi = mid
+    return lo
+
+
+# Overwrite above definitions with a fast C implementation
+try:
+    from _bisect import *
+except ImportError:
+    pass
diff --git a/python-stdlib/bisect/manifest.py b/python-stdlib/bisect/manifest.py
new file mode 100644
index 000000000..5ba5a9a6b
--- /dev/null
+++ b/python-stdlib/bisect/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.5.0")
+
+module("bisect.py")
diff --git a/cmd/cmd.py b/python-stdlib/cmd/cmd.py
similarity index 82%
rename from cmd/cmd.py
rename to python-stdlib/cmd/cmd.py
index 4c1bdab30..447ea1489 100644
--- a/cmd/cmd.py
+++ b/python-stdlib/cmd/cmd.py
@@ -51,14 +51,14 @@
 completions have also been stripped out.
 """
 
-#import string, sys
-import sys	# MiroPython doesn't yet have a string module
+import sys
 
 __all__ = ["Cmd"]
 
-PROMPT = '(Cmd) '
-#IDENTCHARS = string.ascii_letters + string.digits + '_'
-IDENTCHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
+PROMPT = "(Cmd) "
+# This is equivalent to string.ascii_letters + string.digits + '_'
+IDENTCHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
+
 
 class Cmd:
     """A simple framework for writing line-oriented command interpreters.
@@ -72,10 +72,11 @@ class Cmd:
     in order to inherit Cmd's methods and encapsulate action methods.
 
     """
+
     prompt = PROMPT
     identchars = IDENTCHARS
-    ruler = '='
-    lastcmd = ''
+    ruler = "="
+    lastcmd = ""
     intro = None
     doc_leader = ""
     doc_header = "Documented commands (type help <topic>):"
@@ -114,7 +115,7 @@ def cmdloop(self, intro=None):
             if intro is not None:
                 self.intro = intro
             if self.intro:
-                self.stdout.write(str(self.intro)+"\n")
+                self.stdout.write(str(self.intro) + "\n")
             stop = None
             while not stop:
                 if self.cmdqueue:
@@ -124,15 +125,15 @@ def cmdloop(self, intro=None):
                         try:
                             line = input(self.prompt)
                         except EOFError:
-                            line = 'EOF'
+                            line = "EOF"
                     else:
                         self.stdout.write(self.prompt)
                         self.stdout.flush()
                         line = self.stdin.readline()
                         if not len(line):
-                            line = 'EOF'
+                            line = "EOF"
                         else:
-                            line = line.rstrip('\r\n')
+                            line = line.rstrip("\r\n")
                 line = self.precmd(line)
                 stop = self.onecmd(line)
                 stop = self.postcmd(stop, line)
@@ -170,15 +171,16 @@ def parseline(self, line):
         line = line.strip()
         if not line:
             return None, None, line
-        elif line[0] == '?':
-            line = 'help ' + line[1:]
-        elif line[0] == '!':
-            if hasattr(self, 'do_shell'):
-                line = 'shell ' + line[1:]
+        elif line[0] == "?":
+            line = "help " + line[1:]
+        elif line[0] == "!":
+            if hasattr(self, "do_shell"):
+                line = "shell " + line[1:]
             else:
                 return None, None, line
         i, n = 0, len(line)
-        while i < n and line[i] in self.identchars: i = i+1
+        while i < n and line[i] in self.identchars:
+            i = i + 1
         cmd, arg = line[:i], line[i:].strip()
         return cmd, arg, line
 
@@ -198,13 +200,13 @@ def onecmd(self, line):
         if cmd is None:
             return self.default(line)
         self.lastcmd = line
-        if line == 'EOF' :
-            self.lastcmd = ''
-        if cmd == '':
+        if line == "EOF":
+            self.lastcmd = ""
+        if cmd == "":
             return self.default(line)
         else:
             try:
-                func = getattr(self, 'do_' + cmd)
+                func = getattr(self, "do_" + cmd)
             except AttributeError:
                 return self.default(line)
             return func(arg)
@@ -226,7 +228,7 @@ def default(self, line):
         returns.
 
         """
-        self.stdout.write('*** Unknown syntax: %s\n'%line)
+        self.stdout.write("*** Unknown syntax: %s\n" % line)
 
     def get_names(self):
         # This method used to pull in base class attributes
@@ -238,9 +240,9 @@ def do_help(self, arg):
         if arg:
             # XXX check arg syntax
             try:
-                func = getattr(self, 'help_' + arg)
+                func = getattr(self, "help_" + arg)
             except AttributeError:
-                self.stdout.write("%s\n"%str(self.nohelp % (arg,)))
+                self.stdout.write("%s\n" % str(self.nohelp % (arg,)))
                 return
             func()
         else:
@@ -249,33 +251,33 @@ def do_help(self, arg):
             cmds_undoc = []
             help = {}
             for name in names:
-                if name[:5] == 'help_':
-                    help[name[5:]]=1
+                if name[:5] == "help_":
+                    help[name[5:]] = 1
             names.sort()
             # There can be duplicates if routines overridden
-            prevname = ''
+            prevname = ""
             for name in names:
-                if name[:3] == 'do_':
+                if name[:3] == "do_":
                     if name == prevname:
                         continue
                     prevname = name
-                    cmd=name[3:]
+                    cmd = name[3:]
                     if cmd in help:
                         cmds_doc.append(cmd)
                         del help[cmd]
                     else:
                         cmds_undoc.append(cmd)
-            self.stdout.write("%s\n"%str(self.doc_leader))
-            self.print_topics(self.doc_header,   cmds_doc,   15,80)
-            self.print_topics(self.misc_header,  list(help.keys()),15,80)
-            self.print_topics(self.undoc_header, cmds_undoc, 15,80)
+            self.stdout.write("%s\n" % str(self.doc_leader))
+            self.print_topics(self.doc_header, cmds_doc, 15, 80)
+            self.print_topics(self.misc_header, list(help.keys()), 15, 80)
+            self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
 
     def print_topics(self, header, cmds, cmdlen, maxcol):
         if cmds:
-            self.stdout.write("%s\n"%str(header))
+            self.stdout.write("%s\n" % str(header))
             if self.ruler:
-                self.stdout.write("%s\n"%str(self.ruler * len(header)))
-            self.columnize(cmds, maxcol-1)
+                self.stdout.write("%s\n" % str(self.ruler * len(header)))
+            self.columnize(cmds, maxcol - 1)
             self.stdout.write("\n")
 
     def columnize(self, list, displaywidth=80):
@@ -288,24 +290,22 @@ def columnize(self, list, displaywidth=80):
             self.stdout.write("<empty>\n")
             return
 
-        nonstrings = [i for i in range(len(list))
-                        if not isinstance(list[i], str)]
+        nonstrings = [i for i in range(len(list)) if not isinstance(list[i], str)]
         if nonstrings:
-            raise TypeError("list[i] not a string for i in %s"
-                            % ", ".join(map(str, nonstrings)))
+            raise TypeError("list[i] not a string for i in %s" % ", ".join(map(str, nonstrings)))
         size = len(list)
         if size == 1:
-            self.stdout.write('%s\n'%str(list[0]))
+            self.stdout.write("%s\n" % str(list[0]))
             return
         # Try every row count from 1 upwards
         for nrows in range(1, len(list)):
-            ncols = (size+nrows-1) // nrows
+            ncols = (size + nrows - 1) // nrows
             colwidths = []
             totwidth = -2
             for col in range(ncols):
                 colwidth = 0
                 for row in range(nrows):
-                    i = row + nrows*col
+                    i = row + nrows * col
                     if i >= size:
                         break
                     x = list[i]
@@ -323,7 +323,7 @@ def columnize(self, list, displaywidth=80):
         for row in range(nrows):
             texts = []
             for col in range(ncols):
-                i = row + nrows*col
+                i = row + nrows * col
                 if i >= size:
                     x = ""
                 else:
@@ -332,6 +332,6 @@ def columnize(self, list, displaywidth=80):
             while texts and not texts[-1]:
                 del texts[-1]
             for col in range(len(texts)):
-                #texts[col] = texts[col].ljust(colwidths[col])
-                texts[col] = '%-*s' % (colwidths[col], texts[col])
-            self.stdout.write("%s\n"%str("  ".join(texts)))
+                # texts[col] = texts[col].ljust(colwidths[col])
+                texts[col] = "%-*s" % (colwidths[col], texts[col])
+            self.stdout.write("%s\n" % str("  ".join(texts)))
diff --git a/python-stdlib/cmd/manifest.py b/python-stdlib/cmd/manifest.py
new file mode 100644
index 000000000..910352ee7
--- /dev/null
+++ b/python-stdlib/cmd/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="3.4.1")
+
+module("cmd.py")
diff --git a/collections.defaultdict/collections/defaultdict.py b/python-stdlib/collections-defaultdict/collections/defaultdict.py
similarity index 93%
rename from collections.defaultdict/collections/defaultdict.py
rename to python-stdlib/collections-defaultdict/collections/defaultdict.py
index baf56592b..2d5383282 100644
--- a/collections.defaultdict/collections/defaultdict.py
+++ b/python-stdlib/collections-defaultdict/collections/defaultdict.py
@@ -1,5 +1,4 @@
 class defaultdict:
-
     @staticmethod
     def __new__(cls, default_factory=None, **kwargs):
         # Some code (e.g. urllib.urlparse) expects that basic defaultdict
@@ -27,6 +26,9 @@ def __setitem__(self, key, v):
     def __delitem__(self, key):
         del self.d[key]
 
+    def __contains__(self, key):
+        return key in self.d
+
     def __missing__(self, key):
         if self.default_factory is None:
             raise KeyError(key)
diff --git a/python-stdlib/collections-defaultdict/manifest.py b/python-stdlib/collections-defaultdict/manifest.py
new file mode 100644
index 000000000..e5c06e668
--- /dev/null
+++ b/python-stdlib/collections-defaultdict/manifest.py
@@ -0,0 +1,6 @@
+metadata(version="0.3.0")
+
+# Originally written by Paul Sokolovsky.
+
+require("collections")
+package("collections")
diff --git a/python-stdlib/collections-defaultdict/test_defaultdict.py b/python-stdlib/collections-defaultdict/test_defaultdict.py
new file mode 100644
index 000000000..2b052fc4e
--- /dev/null
+++ b/python-stdlib/collections-defaultdict/test_defaultdict.py
@@ -0,0 +1,10 @@
+from collections import defaultdict
+
+d = defaultdict.defaultdict(lambda: 42)
+assert d[1] == 42
+d[2] = 3
+assert d[2] == 3
+del d[1]
+assert d[1] == 42
+
+assert "foo" not in d
diff --git a/python-stdlib/collections/collections/__init__.py b/python-stdlib/collections/collections/__init__.py
new file mode 100644
index 000000000..36dfc1c41
--- /dev/null
+++ b/python-stdlib/collections/collections/__init__.py
@@ -0,0 +1,12 @@
+# Replace built-in collections module.
+from ucollections import *
+
+# Provide optional dependencies (which may be installed separately).
+try:
+    from .defaultdict import defaultdict
+except ImportError:
+    pass
+
+
+class MutableMapping:
+    pass
diff --git a/python-stdlib/collections/manifest.py b/python-stdlib/collections/manifest.py
new file mode 100644
index 000000000..0ce56d1fa
--- /dev/null
+++ b/python-stdlib/collections/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.2.0")
+
+package("collections")
diff --git a/contextlib/contextlib.py b/python-stdlib/contextlib/contextlib.py
similarity index 96%
rename from contextlib/contextlib.py
rename to python-stdlib/contextlib/contextlib.py
index aca58d711..3e598b4b6 100644
--- a/contextlib/contextlib.py
+++ b/python-stdlib/contextlib/contextlib.py
@@ -29,10 +29,13 @@ class closing(object):
             f.close()
 
     """
+
     def __init__(self, thing):
         self.thing = thing
+
     def __enter__(self):
         return self.thing
+
     def __exit__(self, *exc_info):
         self.thing.close()
 
@@ -66,6 +69,7 @@ def __exit__(self, exctype, excinst, exctb):
         # See http://bugs.python.org/issue12029 for more details
         return exctype is not None and issubclass(exctype, self._exceptions)
 
+
 # Inspired by discussions on http://bugs.python.org/issue13585
 class ExitStack(object):
     """Context manager for dynamic management of a stack of exit callbacks
@@ -79,20 +83,23 @@ class ExitStack(object):
             # in the list raise an exception
 
     """
+
     def __init__(self):
-        self._exit_callbacks = deque()
+        self._exit_callbacks = []
 
     def pop_all(self):
         """Preserve the context stack by transferring it to a new instance"""
         new_stack = type(self)()
         new_stack._exit_callbacks = self._exit_callbacks
-        self._exit_callbacks = deque()
+        self._exit_callbacks = []
         return new_stack
 
     def _push_cm_exit(self, cm, cm_exit):
         """Helper to correctly register callbacks to __exit__ methods"""
+
         def _exit_wrapper(*exc_details):
             return cm_exit(cm, *exc_details)
+
         self.push(_exit_wrapper)
 
     def push(self, exit):
@@ -113,17 +120,19 @@ def push(self, exit):
             self._exit_callbacks.append(exit)
         else:
             self._push_cm_exit(exit, exit_method)
-        return exit # Allow use as a decorator
+        return exit  # Allow use as a decorator
 
     def callback(self, callback, *args, **kwds):
         """Registers an arbitrary callback and arguments.
 
         Cannot suppress exceptions.
         """
+
         def _exit_wrapper(exc_type, exc, tb):
             callback(*args, **kwds)
+
         self.push(_exit_wrapper)
-        return callback # Allow use as a decorator
+        return callback  # Allow use as a decorator
 
     def enter_context(self, cm):
         """Enters the supplied context manager
diff --git a/python-stdlib/contextlib/manifest.py b/python-stdlib/contextlib/manifest.py
new file mode 100644
index 000000000..3e05bca18
--- /dev/null
+++ b/python-stdlib/contextlib/manifest.py
@@ -0,0 +1,6 @@
+metadata(description="Port of contextlib for micropython", version="3.4.4")
+
+require("ucontextlib")
+require("collections")
+
+module("contextlib.py")
diff --git a/python-stdlib/contextlib/tests.py b/python-stdlib/contextlib/tests.py
new file mode 100644
index 000000000..c122c452e
--- /dev/null
+++ b/python-stdlib/contextlib/tests.py
@@ -0,0 +1,418 @@
+import sys
+import unittest
+from contextlib import closing, suppress, ExitStack
+
+
+class ClosingTestCase(unittest.TestCase):
+    class Closable:
+        def __init__(self):
+            self.closed = False
+
+        def close(self):
+            self.closed = True
+
+    def test_closing(self):
+        closable = self.Closable()
+        with closing(closable) as c:
+            self.assertFalse(c.closed)
+        self.assertTrue(closable.closed)
+
+    def test_closing_after_error(self):
+        closable = self.Closable()
+        exc = Exception()
+        try:
+            with closing(closable) as c:
+                raise exc
+        except Exception as e:
+            self.assertEqual(exc, e)
+        self.assertTrue(closable.closed)
+
+
+class SuppressTestCase(unittest.TestCase):
+    def test_suppress(self):
+        with suppress(ValueError, TypeError):
+            raise ValueError()
+            raise TypeError()
+        self.assertTrue(True)
+
+
+class TestExitStack(unittest.TestCase):
+    # @support.requires_docstrings
+    def _test_instance_docs(self):
+        # Issue 19330: ensure context manager instances have good docstrings
+        cm_docstring = ExitStack.__doc__
+        obj = ExitStack()
+        self.assertEqual(obj.__doc__, cm_docstring)
+
+    def test_no_resources(self):
+        with ExitStack():
+            pass
+
+    def test_callback(self):
+        expected = [
+            ((), {}),
+            ((1,), {}),
+            ((1, 2), {}),
+            ((), dict(example=1)),
+            ((1,), dict(example=1)),
+            ((1, 2), dict(example=1)),
+        ]
+        result = []
+
+        def _exit(*args, **kwds):
+            """Test metadata propagation"""
+            result.append((args, kwds))
+
+        with ExitStack() as stack:
+            for args, kwds in reversed(expected):
+                if args and kwds:
+                    f = stack.callback(_exit, *args, **kwds)
+                elif args:
+                    f = stack.callback(_exit, *args)
+                elif kwds:
+                    f = stack.callback(_exit, **kwds)
+                else:
+                    f = stack.callback(_exit)
+                self.assertIs(f, _exit)
+            # for wrapper in stack._exit_callbacks:
+            #     self.assertIs(wrapper.__wrapped__, _exit)
+            #     self.assertNotEqual(wrapper.__name__, _exit.__name__)
+            #     self.assertIsNone(wrapper.__doc__, _exit.__doc__)
+        self.assertEqual(result, expected)
+
+    def test_push(self):
+        exc_raised = ZeroDivisionError
+
+        def _expect_exc(exc_type, exc, exc_tb):
+            self.assertIs(exc_type, exc_raised)
+
+        def _suppress_exc(*exc_details):
+            return True
+
+        def _expect_ok(exc_type, exc, exc_tb):
+            self.assertIsNone(exc_type)
+            self.assertIsNone(exc)
+            self.assertIsNone(exc_tb)
+
+        class ExitCM(object):
+            def __init__(self, check_exc):
+                self.check_exc = check_exc
+
+            def __enter__(self):
+                self.fail("Should not be called!")
+
+            def __exit__(self, *exc_details):
+                self.check_exc(*exc_details)
+
+        with ExitStack() as stack:
+            stack.push(_expect_ok)
+            self.assertIs(tuple(stack._exit_callbacks)[-1], _expect_ok)
+            cm = ExitCM(_expect_ok)
+            stack.push(cm)
+            # self.assertIs(stack._exit_callbacks[-1].__self__, cm)
+            stack.push(_suppress_exc)
+            self.assertIs(tuple(stack._exit_callbacks)[-1], _suppress_exc)
+            cm = ExitCM(_expect_exc)
+            stack.push(cm)
+            # self.assertIs(stack._exit_callbacks[-1].__self__, cm)
+            stack.push(_expect_exc)
+            self.assertIs(tuple(stack._exit_callbacks)[-1], _expect_exc)
+            stack.push(_expect_exc)
+            self.assertIs(tuple(stack._exit_callbacks)[-1], _expect_exc)
+            1 / 0
+
+    def test_enter_context(self):
+        class TestCM(object):
+            def __enter__(self):
+                result.append(1)
+
+            def __exit__(self, *exc_details):
+                result.append(3)
+
+        result = []
+        cm = TestCM()
+        with ExitStack() as stack:
+
+            @stack.callback  # Registered first => cleaned up last
+            def _exit():
+                result.append(4)
+
+            self.assertIsNotNone(_exit)
+            stack.enter_context(cm)
+            # self.assertIs(stack._exit_callbacks[-1].__self__, cm)
+            result.append(2)
+        self.assertEqual(result, [1, 2, 3, 4])
+
+    def test_close(self):
+        result = []
+        with ExitStack() as stack:
+
+            @stack.callback
+            def _exit():
+                result.append(1)
+
+            self.assertIsNotNone(_exit)
+            stack.close()
+            result.append(2)
+        self.assertEqual(result, [1, 2])
+
+    def test_pop_all(self):
+        result = []
+        with ExitStack() as stack:
+
+            @stack.callback
+            def _exit():
+                result.append(3)
+
+            self.assertIsNotNone(_exit)
+            new_stack = stack.pop_all()
+            result.append(1)
+        result.append(2)
+        new_stack.close()
+        self.assertEqual(result, [1, 2, 3])
+
+    def test_exit_raise(self):
+        with self.assertRaises(ZeroDivisionError):
+            with ExitStack() as stack:
+                stack.push(lambda *exc: False)
+                1 / 0
+
+    def test_exit_suppress(self):
+        with ExitStack() as stack:
+            stack.push(lambda *exc: True)
+            1 / 0
+
+    def test_exit_exception_chaining_reference(self):
+        # Sanity check to make sure that ExitStack chaining matches
+        # actual nested with statements
+        exc_chain = []
+
+        class RaiseExc:
+            def __init__(self, exc):
+                self.exc = exc
+
+            def __enter__(self):
+                return self
+
+            def __exit__(self, *exc_details):
+                exc_chain.append(exc_details[0])
+                raise self.exc
+
+        class RaiseExcWithContext:
+            def __init__(self, outer, inner):
+                self.outer = outer
+                self.inner = inner
+
+            def __enter__(self):
+                return self
+
+            def __exit__(self, *exc_details):
+                try:
+                    exc_chain.append(exc_details[0])
+                    raise self.inner
+                except:
+                    exc_chain.append(sys.exc_info()[0])
+                    raise self.outer
+
+        class SuppressExc:
+            def __enter__(self):
+                return self
+
+            def __exit__(self, *exc_details):
+                type(self).saved_details = exc_details
+                return True
+
+        try:
+            with RaiseExc(IndexError):
+                with RaiseExcWithContext(KeyError, AttributeError):
+                    with SuppressExc():
+                        with RaiseExc(ValueError):
+                            1 / 0
+        except IndexError as exc:
+            # self.assertIsInstance(exc.__context__, KeyError)
+            # self.assertIsInstance(exc.__context__.__context__, AttributeError)
+            # Inner exceptions were suppressed
+            # self.assertIsNone(exc.__context__.__context__.__context__)
+            exc_chain.append(type(exc))
+            assert tuple(exc_chain) == (
+                ZeroDivisionError,
+                None,
+                AttributeError,
+                KeyError,
+                IndexError,
+            )
+        else:
+            self.fail("Expected IndexError, but no exception was raised")
+        # Check the inner exceptions
+        inner_exc = SuppressExc.saved_details[1]
+        self.assertIsInstance(inner_exc, ValueError)
+        # self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
+
+    def test_exit_exception_chaining(self):
+        # Ensure exception chaining matches the reference behaviour
+        exc_chain = []
+
+        def raise_exc(exc):
+            frame_exc = sys.exc_info()[0]
+            if frame_exc is not None:
+                exc_chain.append(frame_exc)
+            exc_chain.append(exc)
+            raise exc
+
+        saved_details = None
+
+        def suppress_exc(*exc_details):
+            nonlocal saved_details
+            saved_details = exc_details
+            assert exc_chain[-1] == exc_details[0]
+            exc_chain[-1] = None
+            return True
+
+        try:
+            with ExitStack() as stack:
+                stack.callback(raise_exc, IndexError)
+                stack.callback(raise_exc, KeyError)
+                stack.callback(raise_exc, AttributeError)
+                stack.push(suppress_exc)
+                stack.callback(raise_exc, ValueError)
+                1 / 0
+        except IndexError as exc:
+            # self.assertIsInstance(exc.__context__, KeyError)
+            # self.assertIsInstance(exc.__context__.__context__, AttributeError)
+            # Inner exceptions were suppressed
+            # self.assertIsNone(exc.__context__.__context__.__context__)
+            assert tuple(exc_chain) == (
+                ZeroDivisionError,
+                None,
+                AttributeError,
+                KeyError,
+                IndexError,
+            )
+        else:
+            self.fail("Expected IndexError, but no exception was raised")
+        # Check the inner exceptions
+        inner_exc = saved_details[1]
+        self.assertIsInstance(inner_exc, ValueError)
+        # self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
+
+    def test_exit_exception_non_suppressing(self):
+        # http://bugs.python.org/issue19092
+        def raise_exc(exc):
+            raise exc
+
+        def suppress_exc(*exc_details):
+            return True
+
+        try:
+            with ExitStack() as stack:
+                stack.callback(lambda: None)
+                stack.callback(raise_exc, IndexError)
+        except Exception as exc:
+            self.assertIsInstance(exc, IndexError)
+        else:
+            self.fail("Expected IndexError, but no exception was raised")
+
+        try:
+            with ExitStack() as stack:
+                stack.callback(raise_exc, KeyError)
+                stack.push(suppress_exc)
+                stack.callback(raise_exc, IndexError)
+        except Exception as exc:
+            self.assertIsInstance(exc, KeyError)
+        else:
+            self.fail("Expected KeyError, but no exception was raised")
+
+    def _test_exit_exception_with_correct_context(self):
+        # http://bugs.python.org/issue20317
+        @contextmanager
+        def gets_the_context_right(exc):
+            try:
+                yield
+            finally:
+                raise exc
+
+        exc1 = Exception(1)
+        exc2 = Exception(2)
+        exc3 = Exception(3)
+        exc4 = Exception(4)
+
+        # The contextmanager already fixes the context, so prior to the
+        # fix, ExitStack would try to fix it *again* and get into an
+        # infinite self-referential loop
+        try:
+            with ExitStack() as stack:
+                stack.enter_context(gets_the_context_right(exc4))
+                stack.enter_context(gets_the_context_right(exc3))
+                stack.enter_context(gets_the_context_right(exc2))
+                raise exc1
+        except Exception as exc:
+            self.assertIs(exc, exc4)
+            self.assertIs(exc.__context__, exc3)
+            self.assertIs(exc.__context__.__context__, exc2)
+            self.assertIs(exc.__context__.__context__.__context__, exc1)
+            self.assertIsNone(exc.__context__.__context__.__context__.__context__)
+
+    def _test_exit_exception_with_existing_context(self):
+        # Addresses a lack of test coverage discovered after checking in a
+        # fix for issue 20317 that still contained debugging code.
+        def raise_nested(inner_exc, outer_exc):
+            try:
+                raise inner_exc
+            finally:
+                raise outer_exc
+
+        exc1 = Exception(1)
+        exc2 = Exception(2)
+        exc3 = Exception(3)
+        exc4 = Exception(4)
+        exc5 = Exception(5)
+        try:
+            with ExitStack() as stack:
+                stack.callback(raise_nested, exc4, exc5)
+                stack.callback(raise_nested, exc2, exc3)
+                raise exc1
+        except Exception as exc:
+            self.assertIs(exc, exc5)
+            self.assertIs(exc.__context__, exc4)
+            self.assertIs(exc.__context__.__context__, exc3)
+            self.assertIs(exc.__context__.__context__.__context__, exc2)
+            self.assertIs(exc.__context__.__context__.__context__.__context__, exc1)
+            self.assertIsNone(exc.__context__.__context__.__context__.__context__.__context__)
+
+    def test_body_exception_suppress(self):
+        def suppress_exc(*exc_details):
+            return True
+
+        try:
+            with ExitStack() as stack:
+                stack.push(suppress_exc)
+                1 / 0
+        except IndexError as exc:
+            self.fail("Expected no exception, got IndexError")
+
+    def test_exit_exception_chaining_suppress(self):
+        with ExitStack() as stack:
+            stack.push(lambda *exc: True)
+            stack.push(lambda *exc: 1 / 0)
+            stack.push(lambda *exc: {}[1])
+
+    def test_excessive_nesting(self):
+        # The original implementation would die with RecursionError here
+        with ExitStack() as stack:
+            for i in range(5000):
+                stack.callback(int)
+
+    def test_instance_bypass(self):
+        class Example(object):
+            pass
+
+        cm = Example()
+        cm.__exit__ = object()
+        stack = ExitStack()
+        self.assertRaises(AttributeError, stack.enter_context, cm)
+        stack.push(cm)
+        self.assertIs(tuple(stack._exit_callbacks)[-1], cm)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/copy/copy.py b/python-stdlib/copy/copy.py
similarity index 89%
rename from copy/copy.py
rename to python-stdlib/copy/copy.py
index d9948dfde..0a9283777 100644
--- a/copy/copy.py
+++ b/python-stdlib/copy/copy.py
@@ -49,13 +49,22 @@ class instances).
 """
 
 import types
-#import weakref
-#from copyreg import dispatch_table
-#import builtins
+
+# import weakref
+# from copyreg import dispatch_table
+# import builtins
+
 
 class Error(Exception):
     pass
-error = Error   # backward compatibility
+
+
+error = Error  # backward compatibility
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    OrderedDict = None
 
 try:
     from org.python.core import PyStringMap
@@ -64,6 +73,7 @@ class Error(Exception):
 
 __all__ = ["Error", "copy", "deepcopy"]
 
+
 def copy(x):
     """Shallow copy operation on arbitrary Python objects.
 
@@ -102,33 +112,54 @@ def copy(x):
 
 _copy_dispatch = d = {}
 
+
 def _copy_immutable(x):
     return x
-for t in (type(None), int, float, bool, str, tuple,
-          type, range,
-          types.BuiltinFunctionType, type(Ellipsis),
-          types.FunctionType):
+
+
+for t in (
+    type(None),
+    int,
+    float,
+    bool,
+    str,
+    tuple,
+    type,
+    range,
+    types.BuiltinFunctionType,
+    type(Ellipsis),
+    types.FunctionType,
+):
     d[t] = _copy_immutable
 t = getattr(types, "CodeType", None)
 if t is not None:
     d[t] = _copy_immutable
-#for name in ("complex", "unicode"):
+# for name in ("complex", "unicode"):
 #    t = getattr(builtins, name, None)
 #    if t is not None:
 #        d[t] = _copy_immutable
 
+
 def _copy_with_constructor(x):
     return type(x)(x)
+
+
 for t in (list, dict, set):
     d[t] = _copy_with_constructor
+if OrderedDict is not None:
+    d[OrderedDict] = _copy_with_constructor
+
 
 def _copy_with_copy_method(x):
     return x.copy()
+
+
 if PyStringMap is not None:
     d[PyStringMap] = _copy_with_copy_method
 
 del d
 
+
 def deepcopy(x, memo=None, _nil=[]):
     """Deep copy operation on arbitrary Python objects.
 
@@ -151,7 +182,7 @@ def deepcopy(x, memo=None, _nil=[]):
     else:
         try:
             issc = issubclass(cls, type)
-        except TypeError: # cls is not a class (old Boost; see SF #502085)
+        except TypeError:  # cls is not a class (old Boost; see SF #502085)
             issc = 0
         if issc:
             y = _deepcopy_atomic(x, memo)
@@ -172,20 +203,23 @@ def deepcopy(x, memo=None, _nil=[]):
                         if reductor:
                             rv = reductor()
                         else:
-                            raise Error(
-                                "un(deep)copyable object of type %s" % cls)
+                            raise Error("un(deep)copyable object of type %s" % cls)
                 y = _reconstruct(x, rv, 1, memo)
 
     # If is its own copy, don't memoize.
     if y is not x:
         memo[d] = y
-        _keep_alive(x, memo) # Make sure x lives at least as long as d
+        _keep_alive(x, memo)  # Make sure x lives at least as long as d
     return y
 
+
 _deepcopy_dispatch = d = {}
 
+
 def _deepcopy_atomic(x, memo):
     return x
+
+
 d[type(None)] = _deepcopy_atomic
 d[type(Ellipsis)] = _deepcopy_atomic
 d[int] = _deepcopy_atomic
@@ -205,7 +239,8 @@ def _deepcopy_atomic(x, memo):
 d[range] = _deepcopy_atomic
 d[types.BuiltinFunctionType] = _deepcopy_atomic
 d[types.FunctionType] = _deepcopy_atomic
-#d[weakref.ref] = _deepcopy_atomic
+# d[weakref.ref] = _deepcopy_atomic
+
 
 def _deepcopy_list(x, memo):
     y = []
@@ -213,8 +248,11 @@ def _deepcopy_list(x, memo):
     for a in x:
         y.append(deepcopy(a, memo))
     return y
+
+
 d[list] = _deepcopy_list
 
+
 def _deepcopy_tuple(x, memo):
     y = []
     for a in x:
@@ -232,22 +270,33 @@ def _deepcopy_tuple(x, memo):
     else:
         y = x
     return y
+
+
 d[tuple] = _deepcopy_tuple
 
+
 def _deepcopy_dict(x, memo):
-    y = {}
+    y = type(x)()
     memo[id(x)] = y
     for key, value in x.items():
         y[deepcopy(key, memo)] = deepcopy(value, memo)
     return y
+
+
 d[dict] = _deepcopy_dict
+if OrderedDict is not None:
+    d[OrderedDict] = _deepcopy_dict
 if PyStringMap is not None:
     d[PyStringMap] = _deepcopy_dict
 
-def _deepcopy_method(x, memo): # Copy instance methods
+
+def _deepcopy_method(x, memo):  # Copy instance methods
     return type(x)(x.__func__, deepcopy(x.__self__, memo))
+
+
 _deepcopy_dispatch[types.MethodType] = _deepcopy_method
 
+
 def _keep_alive(x, memo):
     """Keeps a reference to the object x in the memo.
 
@@ -262,7 +311,8 @@ def _keep_alive(x, memo):
         memo[id(memo)].append(x)
     except KeyError:
         # aha, this is the first one :-)
-        memo[id(memo)]=[x]
+        memo[id(memo)] = [x]
+
 
 def _reconstruct(x, info, deep, memo=None):
     if isinstance(info, str):
@@ -293,7 +343,7 @@ def _reconstruct(x, info, deep, memo=None):
     if state:
         if deep:
             state = deepcopy(state, memo)
-        if hasattr(y, '__setstate__'):
+        if hasattr(y, "__setstate__"):
             y.__setstate__(state)
         else:
             if isinstance(state, tuple) and len(state) == 2:
@@ -319,10 +369,12 @@ def _reconstruct(x, info, deep, memo=None):
             y[key] = value
     return y
 
+
 del d
 
 del types
 
+
 # Helper for instance creation without calling __init__
 class _EmptyClass:
     pass
diff --git a/python-stdlib/copy/manifest.py b/python-stdlib/copy/manifest.py
new file mode 100644
index 000000000..b22ebeb90
--- /dev/null
+++ b/python-stdlib/copy/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="3.3.4")
+
+require("types")
+
+module("copy.py")
diff --git a/python-stdlib/curses.ascii/curses/ascii.py b/python-stdlib/curses.ascii/curses/ascii.py
new file mode 100644
index 000000000..34cb79853
--- /dev/null
+++ b/python-stdlib/curses.ascii/curses/ascii.py
@@ -0,0 +1,177 @@
+"""Constants and membership tests for ASCII characters"""
+
+NUL = 0x00  # ^@
+SOH = 0x01  # ^A
+STX = 0x02  # ^B
+ETX = 0x03  # ^C
+EOT = 0x04  # ^D
+ENQ = 0x05  # ^E
+ACK = 0x06  # ^F
+BEL = 0x07  # ^G
+BS = 0x08  # ^H
+TAB = 0x09  # ^I
+HT = 0x09  # ^I
+LF = 0x0A  # ^J
+NL = 0x0A  # ^J
+VT = 0x0B  # ^K
+FF = 0x0C  # ^L
+CR = 0x0D  # ^M
+SO = 0x0E  # ^N
+SI = 0x0F  # ^O
+DLE = 0x10  # ^P
+DC1 = 0x11  # ^Q
+DC2 = 0x12  # ^R
+DC3 = 0x13  # ^S
+DC4 = 0x14  # ^T
+NAK = 0x15  # ^U
+SYN = 0x16  # ^V
+ETB = 0x17  # ^W
+CAN = 0x18  # ^X
+EM = 0x19  # ^Y
+SUB = 0x1A  # ^Z
+ESC = 0x1B  # ^[
+FS = 0x1C  # ^\
+GS = 0x1D  # ^]
+RS = 0x1E  # ^^
+US = 0x1F  # ^_
+SP = 0x20  # space
+DEL = 0x7F  # delete
+
+controlnames = [
+    "NUL",
+    "SOH",
+    "STX",
+    "ETX",
+    "EOT",
+    "ENQ",
+    "ACK",
+    "BEL",
+    "BS",
+    "HT",
+    "LF",
+    "VT",
+    "FF",
+    "CR",
+    "SO",
+    "SI",
+    "DLE",
+    "DC1",
+    "DC2",
+    "DC3",
+    "DC4",
+    "NAK",
+    "SYN",
+    "ETB",
+    "CAN",
+    "EM",
+    "SUB",
+    "ESC",
+    "FS",
+    "GS",
+    "RS",
+    "US",
+    "SP",
+]
+
+
+def _ctoi(c):
+    if type(c) == type(""):
+        return ord(c)
+    else:
+        return c
+
+
+def isalnum(c):
+    return isalpha(c) or isdigit(c)
+
+
+def isalpha(c):
+    return isupper(c) or islower(c)
+
+
+def isascii(c):
+    return _ctoi(c) <= 127  # ?
+
+
+def isblank(c):
+    return _ctoi(c) in (8, 32)
+
+
+def iscntrl(c):
+    return _ctoi(c) <= 31
+
+
+def isdigit(c):
+    return _ctoi(c) >= 48 and _ctoi(c) <= 57
+
+
+def isgraph(c):
+    return _ctoi(c) >= 33 and _ctoi(c) <= 126
+
+
+def islower(c):
+    return _ctoi(c) >= 97 and _ctoi(c) <= 122
+
+
+def isprint(c):
+    return _ctoi(c) >= 32 and _ctoi(c) <= 126
+
+
+def ispunct(c):
+    return _ctoi(c) != 32 and not isalnum(c)
+
+
+def isspace(c):
+    return _ctoi(c) in (9, 10, 11, 12, 13, 32)
+
+
+def isupper(c):
+    return _ctoi(c) >= 65 and _ctoi(c) <= 90
+
+
+def isxdigit(c):
+    return (
+        isdigit(c) or (_ctoi(c) >= 65 and _ctoi(c) <= 70) or (_ctoi(c) >= 97 and _ctoi(c) <= 102)
+    )
+
+
+def isctrl(c):
+    return _ctoi(c) < 32
+
+
+def ismeta(c):
+    return _ctoi(c) > 127
+
+
+def ascii(c):
+    if type(c) == type(""):
+        return chr(_ctoi(c) & 0x7F)
+    else:
+        return _ctoi(c) & 0x7F
+
+
+def ctrl(c):
+    if type(c) == type(""):
+        return chr(_ctoi(c) & 0x1F)
+    else:
+        return _ctoi(c) & 0x1F
+
+
+def alt(c):
+    if type(c) == type(""):
+        return chr(_ctoi(c) | 0x80)
+    else:
+        return _ctoi(c) | 0x80
+
+
+def unctrl(c):
+    bits = _ctoi(c)
+    if bits == 0x7F:
+        rep = "^?"
+    elif isprint(bits & 0x7F):
+        rep = chr(bits & 0x7F)
+    else:
+        rep = "^" + chr(((bits & 0x7F) | 0x20) + 0x20)
+    if bits & 0x80:
+        return "!" + rep
+    return rep
diff --git a/python-stdlib/curses.ascii/manifest.py b/python-stdlib/curses.ascii/manifest.py
new file mode 100644
index 000000000..643e3d49a
--- /dev/null
+++ b/python-stdlib/curses.ascii/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="3.4.3")
+
+package("curses")
diff --git a/python-stdlib/datetime/datetime.py b/python-stdlib/datetime/datetime.py
new file mode 100644
index 000000000..0f2a89105
--- /dev/null
+++ b/python-stdlib/datetime/datetime.py
@@ -0,0 +1,877 @@
+# datetime.py
+
+import time as _tmod
+
+_DBM = (0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334)
+_DIM = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
+_TIME_SPEC = ("auto", "hours", "minutes", "seconds", "milliseconds", "microseconds")
+
+
+def _leap(y):
+    return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)
+
+
+def _dby(y):
+    # year -> number of days before January 1st of year.
+    Y = y - 1
+    return Y * 365 + Y // 4 - Y // 100 + Y // 400
+
+
+def _dim(y, m):
+    # year, month -> number of days in that month in that year.
+    if m == 2 and _leap(y):
+        return 29
+    return _DIM[m]
+
+
+def _dbm(y, m):
+    # year, month -> number of days in year preceding first day of month.
+    return _DBM[m] + (m > 2 and _leap(y))
+
+
+def _ymd2o(y, m, d):
+    # y, month, day -> ordinal, considering 01-Jan-0001 as day 1.
+    return _dby(y) + _dbm(y, m) + d
+
+
+def _o2ymd(n):
+    # ordinal -> (year, month, day), considering 01-Jan-0001 as day 1.
+    n -= 1
+    n400, n = divmod(n, 146_097)
+    y = n400 * 400 + 1
+    n100, n = divmod(n, 36_524)
+    n4, n = divmod(n, 1_461)
+    n1, n = divmod(n, 365)
+    y += n100 * 100 + n4 * 4 + n1
+    if n1 == 4 or n100 == 4:
+        return y - 1, 12, 31
+    m = (n + 50) >> 5
+    prec = _dbm(y, m)
+    if prec > n:
+        m -= 1
+        prec -= _dim(y, m)
+    n -= prec
+    return y, m, n + 1
+
+
+MINYEAR = 1
+MAXYEAR = 9_999
+
+
+class timedelta:
+    def __init__(
+        self, days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0
+    ):
+        s = (((weeks * 7 + days) * 24 + hours) * 60 + minutes) * 60 + seconds
+        self._us = round((s * 1000 + milliseconds) * 1000 + microseconds)
+
+    def __repr__(self):
+        return "datetime.timedelta(microseconds={})".format(self._us)
+
+    def total_seconds(self):
+        return self._us / 1_000_000
+
+    @property
+    def days(self):
+        return self._tuple(2)[0]
+
+    @property
+    def seconds(self):
+        return self._tuple(3)[1]
+
+    @property
+    def microseconds(self):
+        return self._tuple(3)[2]
+
+    def __add__(self, other):
+        if isinstance(other, datetime):
+            return other.__add__(self)
+        else:
+            us = other._us
+        return timedelta(0, 0, self._us + us)
+
+    def __sub__(self, other):
+        return timedelta(0, 0, self._us - other._us)
+
+    def __neg__(self):
+        return timedelta(0, 0, -self._us)
+
+    def __pos__(self):
+        return self
+
+    def __abs__(self):
+        return -self if self._us < 0 else self
+
+    def __mul__(self, other):
+        return timedelta(0, 0, round(other * self._us))
+
+    __rmul__ = __mul__
+
+    def __truediv__(self, other):
+        if isinstance(other, timedelta):
+            return self._us / other._us
+        else:
+            return timedelta(0, 0, round(self._us / other))
+
+    def __floordiv__(self, other):
+        if isinstance(other, timedelta):
+            return self._us // other._us
+        else:
+            return timedelta(0, 0, int(self._us // other))
+
+    def __mod__(self, other):
+        return timedelta(0, 0, self._us % other._us)
+
+    def __divmod__(self, other):
+        q, r = divmod(self._us, other._us)
+        return q, timedelta(0, 0, r)
+
+    def __eq__(self, other):
+        return self._us == other._us
+
+    def __le__(self, other):
+        return self._us <= other._us
+
+    def __lt__(self, other):
+        return self._us < other._us
+
+    def __ge__(self, other):
+        return self._us >= other._us
+
+    def __gt__(self, other):
+        return self._us > other._us
+
+    def __bool__(self):
+        return self._us != 0
+
+    def __str__(self):
+        return self._format(0x40)
+
+    def __hash__(self):
+        if not hasattr(self, "_hash"):
+            self._hash = hash(self._us)
+        return self._hash
+
+    def isoformat(self):
+        return self._format(0)
+
+    def _format(self, spec=0):
+        if self._us >= 0:
+            td = self
+            g = ""
+        else:
+            td = -self
+            g = "-"
+        d, h, m, s, us = td._tuple(5)
+        ms, us = divmod(us, 1000)
+        r = ""
+        if spec & 0x40:
+            spec &= ~0x40
+            hr = str(h)
+        else:
+            hr = f"{h:02d}"
+        if spec & 0x20:
+            spec &= ~0x20
+            spec |= 0x10
+            r += "UTC"
+        if spec & 0x10:
+            spec &= ~0x10
+            if not g:
+                g = "+"
+        if d:
+            p = "s" if d > 1 else ""
+            r += f"{g}{d} day{p}, "
+            g = ""
+        if spec == 0:
+            spec = 5 if (ms or us) else 3
+        if spec >= 1 or h:
+            r += f"{g}{hr}"
+            if spec >= 2 or m:
+                r += f":{m:02d}"
+                if spec >= 3 or s:
+                    r += f":{s:02d}"
+                    if spec >= 4 or ms:
+                        r += f".{ms:03d}"
+                        if spec >= 5 or us:
+                            r += f"{us:03d}"
+        return r
+
+    def tuple(self):
+        return self._tuple(5)
+
+    def _tuple(self, n):
+        d, us = divmod(self._us, 86_400_000_000)
+        if n == 2:
+            return d, us
+        s, us = divmod(us, 1_000_000)
+        if n == 3:
+            return d, s, us
+        h, s = divmod(s, 3600)
+        m, s = divmod(s, 60)
+        return d, h, m, s, us
+
+
+timedelta.min = timedelta(days=-999_999_999)
+timedelta.max = timedelta(days=999_999_999, hours=23, minutes=59, seconds=59, microseconds=999_999)
+timedelta.resolution = timedelta(microseconds=1)
+
+
+class tzinfo:
+    # abstract class
+    def tzname(self, dt):
+        raise NotImplementedError
+
+    def utcoffset(self, dt):
+        raise NotImplementedError
+
+    def dst(self, dt):
+        raise NotImplementedError
+
+    def fromutc(self, dt):
+        if dt._tz is not self:
+            raise ValueError
+
+        # See original datetime.py for an explanation of this algorithm.
+        dtoff = dt.utcoffset()
+        dtdst = dt.dst()
+        delta = dtoff - dtdst
+        if delta:
+            dt += delta
+            dtdst = dt.dst()
+        return dt + dtdst
+
+    def isoformat(self, dt):
+        return self.utcoffset(dt)._format(0x12)
+
+
+class timezone(tzinfo):
+    def __init__(self, offset, name=None):
+        if not (abs(offset._us) < 86_400_000_000):
+            raise ValueError
+        self._offset = offset
+        self._name = name
+
+    def __repr__(self):
+        return "datetime.timezone({}, {})".format(repr(self._offset), repr(self._name))
+
+    def __eq__(self, other):
+        if isinstance(other, timezone):
+            return self._offset == other._offset
+        return NotImplemented
+
+    def __str__(self):
+        return self.tzname(None)
+
+    def __hash__(self):
+        if not hasattr(self, "_hash"):
+            self._hash = hash((self._offset, self._name))
+        return self._hash
+
+    def utcoffset(self, dt):
+        return self._offset
+
+    def dst(self, dt):
+        return None
+
+    def tzname(self, dt):
+        if self._name:
+            return self._name
+        return self._offset._format(0x22)
+
+    def fromutc(self, dt):
+        return dt + self._offset
+
+
+timezone.utc = timezone(timedelta(0))
+
+
+def _date(y, m, d):
+    if MINYEAR <= y <= MAXYEAR and 1 <= m <= 12 and 1 <= d <= _dim(y, m):
+        return _ymd2o(y, m, d)
+    elif y == 0 and m == 0 and 1 <= d <= 3_652_059:
+        return d
+    else:
+        raise ValueError
+
+
+def _iso2d(s):  # ISO -> date
+    if len(s) < 10 or s[4] != "-" or s[7] != "-":
+        raise ValueError
+    return int(s[0:4]), int(s[5:7]), int(s[8:10])
+
+
+def _d2iso(o):  # date -> ISO
+    return "%04d-%02d-%02d" % _o2ymd(o)
+
+
+class date:
+    def __init__(self, year, month, day):
+        self._ord = _date(year, month, day)
+
+    @classmethod
+    def fromtimestamp(cls, ts):
+        return cls(*_tmod.localtime(ts)[:3])
+
+    @classmethod
+    def today(cls):
+        return cls(*_tmod.localtime()[:3])
+
+    @classmethod
+    def fromordinal(cls, n):
+        return cls(0, 0, n)
+
+    @classmethod
+    def fromisoformat(cls, s):
+        return cls(*_iso2d(s))
+
+    @property
+    def year(self):
+        return self.tuple()[0]
+
+    @property
+    def month(self):
+        return self.tuple()[1]
+
+    @property
+    def day(self):
+        return self.tuple()[2]
+
+    def toordinal(self):
+        return self._ord
+
+    def timetuple(self):
+        y, m, d = self.tuple()
+        yday = _dbm(y, m) + d
+        return (y, m, d, 0, 0, 0, self.weekday(), yday, -1)
+
+    def replace(self, year=None, month=None, day=None):
+        year_, month_, day_ = self.tuple()
+        if year is None:
+            year = year_
+        if month is None:
+            month = month_
+        if day is None:
+            day = day_
+        return date(year, month, day)
+
+    def __add__(self, other):
+        return date.fromordinal(self._ord + other.days)
+
+    def __sub__(self, other):
+        if isinstance(other, date):
+            return timedelta(days=self._ord - other._ord)
+        else:
+            return date.fromordinal(self._ord - other.days)
+
+    def __eq__(self, other):
+        if isinstance(other, date):
+            return self._ord == other._ord
+        else:
+            return False
+
+    def __le__(self, other):
+        return self._ord <= other._ord
+
+    def __lt__(self, other):
+        return self._ord < other._ord
+
+    def __ge__(self, other):
+        return self._ord >= other._ord
+
+    def __gt__(self, other):
+        return self._ord > other._ord
+
+    def weekday(self):
+        return (self._ord + 6) % 7
+
+    def isoweekday(self):
+        return self._ord % 7 or 7
+
+    def isoformat(self):
+        return _d2iso(self._ord)
+
+    def __repr__(self):
+        return "datetime.date(0, 0, {})".format(self._ord)
+
+    __str__ = isoformat
+
+    def __hash__(self):
+        if not hasattr(self, "_hash"):
+            self._hash = hash(self._ord)
+        return self._hash
+
+    def tuple(self):
+        return _o2ymd(self._ord)
+
+
+date.min = date(MINYEAR, 1, 1)
+date.max = date(MAXYEAR, 12, 31)
+date.resolution = timedelta(days=1)
+
+
+def _time(h, m, s, us, fold):
+    if (
+        0 <= h < 24
+        and 0 <= m < 60
+        and 0 <= s < 60
+        and 0 <= us < 1_000_000
+        and (fold == 0 or fold == 1)
+    ) or (h == 0 and m == 0 and s == 0 and 0 < us < 86_400_000_000):
+        return timedelta(0, s, us, 0, m, h)
+    else:
+        raise ValueError
+
+
+def _iso2t(s):
+    hour = 0
+    minute = 0
+    sec = 0
+    usec = 0
+    tz_sign = ""
+    tz_hour = 0
+    tz_minute = 0
+    tz_sec = 0
+    tz_usec = 0
+    l = len(s)
+    i = 0
+    if l < 2:
+        raise ValueError
+    i += 2
+    hour = int(s[i - 2 : i])
+    if l > i and s[i] == ":":
+        i += 3
+        if l - i < 0:
+            raise ValueError
+        minute = int(s[i - 2 : i])
+        if l > i and s[i] == ":":
+            i += 3
+            if l - i < 0:
+                raise ValueError
+            sec = int(s[i - 2 : i])
+            if l > i and s[i] == ".":
+                i += 4
+                if l - i < 0:
+                    raise ValueError
+                usec = 1000 * int(s[i - 3 : i])
+                if l > i and s[i] != "+":
+                    i += 3
+                    if l - i < 0:
+                        raise ValueError
+                    usec += int(s[i - 3 : i])
+    if l > i:
+        if s[i] not in "+-":
+            raise ValueError
+        tz_sign = s[i]
+        i += 6
+        if l - i < 0:
+            raise ValueError
+        tz_hour = int(s[i - 5 : i - 3])
+        tz_minute = int(s[i - 2 : i])
+        if l > i and s[i] == ":":
+            i += 3
+            if l - i < 0:
+                raise ValueError
+            tz_sec = int(s[i - 2 : i])
+            if l > i and s[i] == ".":
+                i += 7
+                if l - i < 0:
+                    raise ValueError
+                tz_usec = int(s[i - 6 : i])
+    if l != i:
+        raise ValueError
+    if tz_sign:
+        td = timedelta(hours=tz_hour, minutes=tz_minute, seconds=tz_sec, microseconds=tz_usec)
+        if tz_sign == "-":
+            td = -td
+        tz = timezone(td)
+    else:
+        tz = None
+    return hour, minute, sec, usec, tz
+
+
+def _t2iso(td, timespec, dt, tz):
+    s = td._format(_TIME_SPEC.index(timespec))
+    if tz is not None:
+        s += tz.isoformat(dt)
+    return s
+
+
+class time:
+    def __init__(self, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0):
+        self._td = _time(hour, minute, second, microsecond, fold)
+        self._tz = tzinfo
+        self._fd = fold
+
+    @classmethod
+    def fromisoformat(cls, s):
+        return cls(*_iso2t(s))
+
+    @property
+    def hour(self):
+        return self.tuple()[0]
+
+    @property
+    def minute(self):
+        return self.tuple()[1]
+
+    @property
+    def second(self):
+        return self.tuple()[2]
+
+    @property
+    def microsecond(self):
+        return self.tuple()[3]
+
+    @property
+    def tzinfo(self):
+        return self._tz
+
+    @property
+    def fold(self):
+        return self._fd
+
+    def replace(
+        self, hour=None, minute=None, second=None, microsecond=None, tzinfo=True, *, fold=None
+    ):
+        h, m, s, us, tz, fl = self.tuple()
+        if hour is None:
+            hour = h
+        if minute is None:
+            minute = m
+        if second is None:
+            second = s
+        if microsecond is None:
+            microsecond = us
+        if tzinfo is True:
+            tzinfo = tz
+        if fold is None:
+            fold = fl
+        return time(hour, minute, second, microsecond, tzinfo, fold=fold)
+
+    def isoformat(self, timespec="auto"):
+        return _t2iso(self._td, timespec, None, self._tz)
+
+    def __repr__(self):
+        return "datetime.time(microsecond={}, tzinfo={}, fold={})".format(
+            self._td._us, repr(self._tz), self._fd
+        )
+
+    __str__ = isoformat
+
+    def __bool__(self):
+        return True
+
+    def __eq__(self, other):
+        if (self._tz == None) ^ (other._tz == None):
+            return False
+        return self._sub(other) == 0
+
+    def __le__(self, other):
+        return self._sub(other) <= 0
+
+    def __lt__(self, other):
+        return self._sub(other) < 0
+
+    def __ge__(self, other):
+        return self._sub(other) >= 0
+
+    def __gt__(self, other):
+        return self._sub(other) > 0
+
+    def _sub(self, other):
+        tz1 = self._tz
+        if (tz1 is None) ^ (other._tz is None):
+            raise TypeError
+        us1 = self._td._us
+        us2 = other._td._us
+        if tz1 is not None:
+            os1 = self.utcoffset()._us
+            os2 = other.utcoffset()._us
+            if os1 != os2:
+                us1 -= os1
+                us2 -= os2
+        return us1 - us2
+
+    def __hash__(self):
+        if not hasattr(self, "_hash"):
+            # fold doesn't make any difference
+            self._hash = hash((self._td, self._tz))
+        return self._hash
+
+    def utcoffset(self):
+        return None if self._tz is None else self._tz.utcoffset(None)
+
+    def dst(self):
+        return None if self._tz is None else self._tz.dst(None)
+
+    def tzname(self):
+        return None if self._tz is None else self._tz.tzname(None)
+
+    def tuple(self):
+        d, h, m, s, us = self._td.tuple()
+        return h, m, s, us, self._tz, self._fd
+
+
+time.min = time(0)
+time.max = time(23, 59, 59, 999_999)
+time.resolution = timedelta.resolution
+
+
+class datetime:
+    def __init__(
+        self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0
+    ):
+        self._d = _date(year, month, day)
+        self._t = _time(hour, minute, second, microsecond, fold)
+        self._tz = tzinfo
+        self._fd = fold
+
+    @classmethod
+    def fromtimestamp(cls, ts, tz=None):
+        if isinstance(ts, float):
+            ts, us = divmod(round(ts * 1_000_000), 1_000_000)
+        else:
+            us = 0
+        if tz is None:
+            raise NotImplementedError
+        else:
+            dt = cls(*_tmod.gmtime(ts)[:6], microsecond=us, tzinfo=tz)
+            dt = tz.fromutc(dt)
+        return dt
+
+    @classmethod
+    def now(cls, tz=None):
+        return cls.fromtimestamp(_tmod.time(), tz)
+
+    @classmethod
+    def fromordinal(cls, n):
+        return cls(0, 0, n)
+
+    @classmethod
+    def fromisoformat(cls, s):
+        d = _iso2d(s)
+        if len(s) <= 12:
+            return cls(*d)
+        t = _iso2t(s[11:])
+        return cls(*(d + t))
+
+    @classmethod
+    def combine(cls, date, time, tzinfo=None):
+        return cls(
+            0, 0, date.toordinal(), 0, 0, 0, time._td._us, tzinfo or time._tz, fold=time._fd
+        )
+
+    @property
+    def year(self):
+        return _o2ymd(self._d)[0]
+
+    @property
+    def month(self):
+        return _o2ymd(self._d)[1]
+
+    @property
+    def day(self):
+        return _o2ymd(self._d)[2]
+
+    @property
+    def hour(self):
+        return self._t.tuple()[1]
+
+    @property
+    def minute(self):
+        return self._t.tuple()[2]
+
+    @property
+    def second(self):
+        return self._t.tuple()[3]
+
+    @property
+    def microsecond(self):
+        return self._t.tuple()[4]
+
+    @property
+    def tzinfo(self):
+        return self._tz
+
+    @property
+    def fold(self):
+        return self._fd
+
+    def __add__(self, other):
+        us = self._t._us + other._us
+        d, us = divmod(us, 86_400_000_000)
+        d += self._d
+        return datetime(0, 0, d, 0, 0, 0, us, self._tz)
+
+    def __sub__(self, other):
+        if isinstance(other, timedelta):
+            return self.__add__(-other)
+        elif isinstance(other, datetime):
+            d, us = self._sub(other)
+            return timedelta(d, 0, us)
+        else:
+            raise TypeError
+
+    def _sub(self, other):
+        # Subtract two datetime instances.
+        tz1 = self._tz
+        if (tz1 is None) ^ (other._tz is None):
+            raise TypeError
+        dt1 = self
+        dt2 = other
+        if tz1 is not None:
+            os1 = dt1.utcoffset()
+            os2 = dt2.utcoffset()
+            if os1 != os2:
+                dt1 -= os1
+                dt2 -= os2
+        D = dt1._d - dt2._d
+        us = dt1._t._us - dt2._t._us
+        d, us = divmod(us, 86_400_000_000)
+        return D + d, us
+
+    def __eq__(self, other):
+        if (self._tz == None) ^ (other._tz == None):
+            return False
+        return self._cmp(other) == 0
+
+    def __le__(self, other):
+        return self._cmp(other) <= 0
+
+    def __lt__(self, other):
+        return self._cmp(other) < 0
+
+    def __ge__(self, other):
+        return self._cmp(other) >= 0
+
+    def __gt__(self, other):
+        return self._cmp(other) > 0
+
+    def _cmp(self, other):
+        # Compare two datetime instances.
+        d, us = self._sub(other)
+        if d < 0:
+            return -1
+        if d > 0:
+            return 1
+
+        if us < 0:
+            return -1
+        if us > 0:
+            return 1
+
+        return 0
+
+    def date(self):
+        return date.fromordinal(self._d)
+
+    def time(self):
+        return time(microsecond=self._t._us, fold=self._fd)
+
+    def timetz(self):
+        return time(microsecond=self._t._us, tzinfo=self._tz, fold=self._fd)
+
+    def replace(
+        self,
+        year=None,
+        month=None,
+        day=None,
+        hour=None,
+        minute=None,
+        second=None,
+        microsecond=None,
+        tzinfo=True,
+        *,
+        fold=None,
+    ):
+        Y, M, D, h, m, s, us, tz, fl = self.tuple()
+        if year is None:
+            year = Y
+        if month is None:
+            month = M
+        if day is None:
+            day = D
+        if hour is None:
+            hour = h
+        if minute is None:
+            minute = m
+        if second is None:
+            second = s
+        if microsecond is None:
+            microsecond = us
+        if tzinfo is True:
+            tzinfo = tz
+        if fold is None:
+            fold = fl
+        return datetime(year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold)
+
+    def astimezone(self, tz=None):
+        if self._tz is tz:
+            return self
+        _tz = self._tz
+        if _tz is None:
+            raise NotImplementedError
+        else:
+            os = _tz.utcoffset(self)
+        utc = self - os
+        utc = utc.replace(tzinfo=tz)
+        return tz.fromutc(utc)
+
+    def utcoffset(self):
+        return None if self._tz is None else self._tz.utcoffset(self)
+
+    def dst(self):
+        return None if self._tz is None else self._tz.dst(self)
+
+    def tzname(self):
+        return None if self._tz is None else self._tz.tzname(self)
+
+    def timetuple(self):
+        if self._tz is None:
+            conv = _tmod.gmtime
+            epoch = datetime.EPOCH.replace(tzinfo=None)
+        else:
+            conv = _tmod.localtime
+            epoch = datetime.EPOCH
+        return conv(round((self - epoch).total_seconds()))
+
+    def toordinal(self):
+        return self._d
+
+    def timestamp(self):
+        if self._tz is None:
+            raise NotImplementedError
+        else:
+            return (self - datetime.EPOCH).total_seconds()
+
+    def weekday(self):
+        return (self._d + 6) % 7
+
+    def isoweekday(self):
+        return self._d % 7 or 7
+
+    def isoformat(self, sep="T", timespec="auto"):
+        return _d2iso(self._d) + sep + _t2iso(self._t, timespec, self, self._tz)
+
+    def __repr__(self):
+        Y, M, D, h, m, s, us, tz, fold = self.tuple()
+        tz = repr(tz)
+        return "datetime.datetime({}, {}, {}, {}, {}, {}, {}, {}, fold={})".format(
+            Y, M, D, h, m, s, us, tz, fold
+        )
+
+    def __str__(self):
+        return self.isoformat(" ")
+
+    def __hash__(self):
+        if not hasattr(self, "_hash"):
+            self._hash = hash((self._d, self._t, self._tz))
+        return self._hash
+
+    def tuple(self):
+        d = _o2ymd(self._d)
+        t = self._t.tuple()[1:]
+        return d + t + (self._tz, self._fd)
+
+
+datetime.EPOCH = datetime(*_tmod.gmtime(0)[:6], tzinfo=timezone.utc)
diff --git a/python-stdlib/datetime/localtz.patch b/python-stdlib/datetime/localtz.patch
new file mode 100644
index 000000000..7a2449d5d
--- /dev/null
+++ b/python-stdlib/datetime/localtz.patch
@@ -0,0 +1,84 @@
+localtz.patch
+
+The CPython's implementation of `datetime.fromtimestamp()`,
+`datetime.astimezone()` and `datetime.timestamp()` for naive datetime objects
+relay on proper management of DST (daylight saving time) by `time.localtime()`
+for the timezone of interest. In the Unix port of MicroPython, this is
+accomplished by properly setting the TZ environment variable, e.g.
+`os.putenv("TZ", "Europe/Rome")`.
+
+Because real boards often lack a supportive `time.localtime()`, the source code
+in `datetime.py` has been removed as to save precious resources. If your board
+provide a proper implementation, you can restore the support to naive datetime
+objects by applying this patch, e.g. `patch -p1 < localtz.patch`.
+
+--- a/datetime.py
++++ b/datetime.py
+@@ -635,7 +635,10 @@ class datetime:
+         else:
+             us = 0
+         if tz is None:
+-            raise NotImplementedError
++            dt = cls(*_tmod.localtime(ts)[:6], microsecond=us, tzinfo=tz)
++            s = (dt - datetime(*_tmod.localtime(ts - 86400)[:6]))._us // 1_000_000 - 86400
++            if s < 0 and dt == datetime(*_tmod.localtime(ts + s)[:6]):
++                dt._fd = 1
+         else:
+             dt = cls(*_tmod.gmtime(ts)[:6], microsecond=us, tzinfo=tz)
+             dt = tz.fromutc(dt)
+@@ -812,13 +815,45 @@ class datetime:
+             return self
+         _tz = self._tz
+         if _tz is None:
+-            raise NotImplementedError
++            ts = int(self._mktime())
++            os = datetime(*_tmod.localtime(ts)[:6]) - datetime(*_tmod.gmtime(ts)[:6])
+         else:
+             os = _tz.utcoffset(self)
+         utc = self - os
+         utc = utc.replace(tzinfo=tz)
+         return tz.fromutc(utc)
+ 
++    def _mktime(self):
++        def local(u):
++            return (datetime(*_tmod.localtime(u)[:6]) - epoch)._us // 1_000_000
++
++        epoch = datetime.EPOCH.replace(tzinfo=None)
++        t, us = divmod((self - epoch)._us, 1_000_000)
++        ts = None
++
++        a = local(t) - t
++        u1 = t - a
++        t1 = local(u1)
++        if t1 == t:
++            u2 = u1 + (86400 if self.fold else -86400)
++            b = local(u2) - u2
++            if a == b:
++                ts = u1
++        else:
++            b = t1 - u1
++        if ts is None:
++            u2 = t - b
++            t2 = local(u2)
++            if t2 == t:
++                ts = u2
++            elif t1 == t:
++                ts = u1
++            elif self.fold:
++                ts = min(u1, u2)
++            else:
++                ts = max(u1, u2)
++        return ts + us / 1_000_000
++
+     def utcoffset(self):
+         return None if self._tz is None else self._tz.utcoffset(self)
+ 
+@@ -842,7 +877,7 @@ class datetime:
+ 
+     def timestamp(self):
+         if self._tz is None:
+-            raise NotImplementedError
++            return self._mktime()
+         else:
+             return (self - datetime.EPOCH).total_seconds()
+ 
diff --git a/python-stdlib/datetime/manifest.py b/python-stdlib/datetime/manifest.py
new file mode 100644
index 000000000..017189cec
--- /dev/null
+++ b/python-stdlib/datetime/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="4.0.0")
+
+# Originally written by Lorenzo Cappelletti.
+
+module("datetime.py")
diff --git a/python-stdlib/datetime/test_datetime.py b/python-stdlib/datetime/test_datetime.py
new file mode 100644
index 000000000..98da458f9
--- /dev/null
+++ b/python-stdlib/datetime/test_datetime.py
@@ -0,0 +1,2271 @@
+# See https://github.com/python/cpython/blob/3.9/Lib/test/datetimetester.py
+#
+# This script can be run in 3 different modes:
+# 1. `python3 test_datetime.py --stdlib`: checks that the tests comply to
+#    CPython's standard datetime library.
+# 2. `python3 test_datetime.py`: runs the tests against datetime.py, using
+#    CPython's standard unittest (which accepts filter options, such as
+#    `-v TestTimeDelta -k tuple`, and provides more verbose output in case
+#    of failure).
+# 3. `micropython test_datetime.py`: runs the tests against datetime.py
+#    using MicroPython's unittest library (which must be available).
+#
+# This script also accepts option `--reorder` which rewrites this file
+# in-place by numbering tests in sequence.
+
+import sys
+
+STDLIB = False
+
+if __name__ == "__main__":
+    while len(sys.argv) > 1:
+        if sys.argv[1] == "--reorder":
+            import fileinput, re
+
+            with fileinput.input(files=sys.argv[0], inplace=True) as f:
+                cases = {}
+                n = 0
+                for line in f:
+                    match = re.match("(\s+def\s+test_)(\w+?)(?:\d+)(\(.+\):)", line)
+                    if match:
+                        prefix, name, suffix = match.groups()
+                        if name != last_name:
+                            if name in cases[case]:
+                                sys.exit(
+                                    f"duplicated test in {case} at line {fileinput.filelineno()}: {name}"
+                                )
+                            cases[case].append(name)
+                            last_name = name
+                            i = 0
+                        print(f"{prefix}{name}{i:02d}{suffix}")
+                        i += 1
+                        n += 1
+                        continue
+
+                    match = re.match("class\s+(Test[\w\d]+)\(", line)
+                    if match:
+                        case = match[1]
+                        if case in cases:
+                            sys.exit(
+                                f"duplicated test case at line {fileinput.filelineno()}: {case}"
+                            )
+                        cases[case] = []
+                        last_name = ""
+
+                    print(line, end="")
+            print(f"Reordered {n} tests in {len(cases)} cases")
+        elif sys.argv[1] == "--stdlib":
+            sys.path.pop(0)
+            STDLIB = True
+        else:
+            break
+        sys.argv.pop(1)
+
+import os
+import time as mod_time
+import datetime as mod_datetime
+from datetime import MAXYEAR, MINYEAR, datetime, date, time, timedelta, timezone, tzinfo
+import unittest
+
+
+# See localtz.patch
+try:
+    datetime.fromtimestamp(0)
+    LOCALTZ = True
+except NotImplementedError:
+    LOCALTZ = False
+
+
+if hasattr(datetime, "EPOCH"):
+    EPOCH = datetime.EPOCH
+else:
+    EPOCH = datetime(*mod_time.gmtime(0)[:6], tzinfo=timezone.utc)
+
+
+def eval_mod(s):
+    return eval(s.replace("datetime.", "mod_datetime."))
+
+
+### timedelta ################################################################
+
+a = timedelta(hours=7)
+b = timedelta(minutes=6)
+c = timedelta(seconds=10)
+us = timedelta(microseconds=1)
+td0 = timedelta(0)
+td1 = timedelta(2, 3, 4)
+td2 = timedelta(2, 3, 4)
+td3 = timedelta(2, 3, 5)
+td4 = timedelta(
+    days=100,
+    weeks=-7,
+    hours=-24 * (100 - 49),
+    minutes=-3,
+    seconds=12,
+    microseconds=(3 * 60 - 12) * 1000000,
+)  # == timedelta(0)
+
+td1h = timedelta(hours=1)
+td1hr = "datetime.timedelta(microseconds={})".format(1 * 3600 * 10**6)
+td10h2m = timedelta(hours=10, minutes=2)
+td10h2mr = "datetime.timedelta(microseconds={})".format((10 * 3600 + 2 * 60) * 10**6)
+tdn10h2m40s = timedelta(hours=-10, minutes=2, seconds=40)
+tdn10h2m40sr = "datetime.timedelta(microseconds={})".format((-10 * 3600 + 2 * 60 + 40) * 10**6)
+td1h2m40s100us = timedelta(hours=1, minutes=2, seconds=40, microseconds=100)
+td1h2m40s100usr = "datetime.timedelta(microseconds={})".format(
+    (1 * 3600 + 2 * 60 + 40) * 10**6 + 100
+)
+
+
+class Test0TimeDelta(unittest.TestCase):
+    def test___init__00(self):
+        self.assertEqual(timedelta(), timedelta(weeks=0, days=0, hours=0, minutes=0, seconds=0))
+
+    def test___init__01(self):
+        self.assertEqual(timedelta(weeks=1), timedelta(days=7))
+
+    def test___init__02(self):
+        self.assertEqual(timedelta(days=1), timedelta(hours=24))
+
+    def test___init__03(self):
+        self.assertEqual(timedelta(hours=1), timedelta(minutes=60))
+
+    def test___init__04(self):
+        self.assertEqual(timedelta(minutes=1), timedelta(seconds=60))
+
+    def test___init__05(self):
+        self.assertEqual(timedelta(seconds=1), timedelta(milliseconds=1000))
+
+    def test___init__06(self):
+        self.assertEqual(timedelta(milliseconds=1), timedelta(microseconds=1000))
+
+    def test___init__07(self):
+        self.assertEqual(timedelta(weeks=1.0 / 7), timedelta(days=1))
+
+    def test___init__08(self):
+        self.assertEqual(timedelta(days=1.0 / 24), timedelta(hours=1))
+
+    def test___init__09(self):
+        self.assertEqual(timedelta(hours=1.0 / 60), timedelta(minutes=1))
+
+    def test___init__10(self):
+        self.assertEqual(timedelta(minutes=1.0 / 60), timedelta(seconds=1))
+
+    def test___init__11(self):
+        self.assertEqual(timedelta(seconds=0.001), timedelta(milliseconds=1))
+
+    def test___init__12(self):
+        self.assertEqual(timedelta(milliseconds=0.001), timedelta(microseconds=1))
+
+    def test___init__13(self):
+        self.assertEqual(td1h, eval_mod(td1hr))
+
+    def test___init__14(self):
+        self.assertEqual(td10h2m, eval_mod(td10h2mr))
+
+    def test___init__15(self):
+        self.assertEqual(tdn10h2m40s, eval_mod(tdn10h2m40sr))
+
+    def test___init__16(self):
+        self.assertEqual(td1h2m40s100us, eval_mod(td1h2m40s100usr))
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__00(self):
+        self.assertEqual(repr(td1h), td1hr)
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__01(self):
+        self.assertEqual(repr(td10h2m), td10h2mr)
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__02(self):
+        self.assertEqual(repr(tdn10h2m40s), tdn10h2m40sr)
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__03(self):
+        self.assertEqual(repr(td1h2m40s100us), td1h2m40s100usr)
+
+    def test___repr__04(self):
+        self.assertEqual(td1, eval_mod(repr(td1)))
+
+    def test_total_seconds00(self):
+        d = timedelta(days=365)
+        self.assertEqual(d.total_seconds(), 31536000.0)
+
+    def test_days00(self):
+        self.assertEqual(td1.days, 2)
+
+    def test_seconds00(self):
+        self.assertEqual(td1.seconds, 3)
+
+    def test_microseconds00(self):
+        self.assertEqual(td1.microseconds, 4)
+
+    def test___add__00(self):
+        self.assertEqual(a + b + c, timedelta(hours=7, minutes=6, seconds=10))
+
+    def test___add__01(self):
+        dt = a + datetime(2010, 1, 1, 12, 30)
+        self.assertEqual(dt, datetime(2010, 1, 1, 12 + 7, 30))
+
+    def test___sub__00(self):
+        self.assertEqual(a - b, timedelta(hours=6, minutes=60 - 6))
+
+    def test___neg__00(self):
+        self.assertEqual(-a, timedelta(hours=-7))
+
+    def test___neg__01(self):
+        self.assertEqual(-b, timedelta(hours=-1, minutes=54))
+
+    def test___neg__02(self):
+        self.assertEqual(-c, timedelta(hours=-1, minutes=59, seconds=50))
+
+    def test___pos__00(self):
+        self.assertEqual(+a, timedelta(hours=7))
+
+    def test___abs__00(self):
+        self.assertEqual(abs(a), a)
+
+    def test___abs__01(self):
+        self.assertEqual(abs(-a), a)
+
+    def test___mul__00(self):
+        self.assertEqual(a * 10, timedelta(hours=70))
+
+    def test___mul__01(self):
+        self.assertEqual(a * 10, 10 * a)
+
+    def test___mul__02(self):
+        self.assertEqual(b * 10, timedelta(minutes=60))
+
+    def test___mul__03(self):
+        self.assertEqual(10 * b, timedelta(minutes=60))
+
+    def test___mul__04(self):
+        self.assertEqual(c * 10, timedelta(seconds=100))
+
+    def test___mul__05(self):
+        self.assertEqual(10 * c, timedelta(seconds=100))
+
+    def test___mul__06(self):
+        self.assertEqual(a * -1, -a)
+
+    def test___mul__07(self):
+        self.assertEqual(b * -2, -b - b)
+
+    def test___mul__08(self):
+        self.assertEqual(c * -2, -c + -c)
+
+    def test___mul__09(self):
+        self.assertEqual(b * (60 * 24), (b * 60) * 24)
+
+    def test___mul__10(self):
+        self.assertEqual(b * (60 * 24), (60 * b) * 24)
+
+    def test___mul__11(self):
+        self.assertEqual(c * 6, timedelta(minutes=1))
+
+    def test___mul__12(self):
+        self.assertEqual(6 * c, timedelta(minutes=1))
+
+    def test___truediv__00(self):
+        self.assertEqual(a / 0.5, timedelta(hours=14))
+
+    def test___truediv__01(self):
+        self.assertEqual(b / 0.5, timedelta(minutes=12))
+
+    def test___truediv__02(self):
+        self.assertEqual(a / 7, timedelta(hours=1))
+
+    def test___truediv__03(self):
+        self.assertEqual(b / 6, timedelta(minutes=1))
+
+    def test___truediv__04(self):
+        self.assertEqual(c / 10, timedelta(seconds=1))
+
+    def test___truediv__05(self):
+        self.assertEqual(a / 10, timedelta(minutes=7 * 6))
+
+    def test___truediv__06(self):
+        self.assertEqual(a / 3600, timedelta(seconds=7))
+
+    def test___truediv__07(self):
+        self.assertEqual(a / a, 1.0)
+
+    def test___truediv__08(self):
+        t = timedelta(hours=1, minutes=24, seconds=19)
+        second = timedelta(seconds=1)
+        self.assertEqual(t / second, 5059.0)
+
+    def test___truediv__09(self):
+        t = timedelta(minutes=2, seconds=30)
+        minute = timedelta(minutes=1)
+        self.assertEqual(t / minute, 2.5)
+
+    def test___floordiv__00(self):
+        self.assertEqual(a // 7, timedelta(hours=1))
+
+    def test___floordiv__01(self):
+        self.assertEqual(b // 6, timedelta(minutes=1))
+
+    def test___floordiv__02(self):
+        self.assertEqual(c // 10, timedelta(seconds=1))
+
+    def test___floordiv__03(self):
+        self.assertEqual(a // 10, timedelta(minutes=7 * 6))
+
+    def test___floordiv__04(self):
+        self.assertEqual(a // 3600, timedelta(seconds=7))
+
+    def test___floordiv__05(self):
+        t = timedelta(hours=1, minutes=24, seconds=19)
+        second = timedelta(seconds=1)
+        self.assertEqual(t // second, 5059)
+
+    def test___floordiv__06(self):
+        t = timedelta(minutes=2, seconds=30)
+        minute = timedelta(minutes=1)
+        self.assertEqual(t // minute, 2)
+
+    def test___mod__00(self):
+        t = timedelta(minutes=2, seconds=30)
+        r = t % timedelta(minutes=1)
+        self.assertEqual(r, timedelta(seconds=30))
+
+    def test___mod__01(self):
+        t = timedelta(minutes=-2, seconds=30)
+        r = t % timedelta(minutes=1)
+        self.assertEqual(r, timedelta(seconds=30))
+
+    def test___divmod__00(self):
+        t = timedelta(minutes=2, seconds=30)
+        q, r = divmod(t, timedelta(minutes=1))
+        self.assertEqual(q, 2)
+        self.assertEqual(r, timedelta(seconds=30))
+
+    def test___divmod__01(self):
+        t = timedelta(minutes=-2, seconds=30)
+        q, r = divmod(t, timedelta(minutes=1))
+        self.assertEqual(q, -2)
+        self.assertEqual(r, timedelta(seconds=30))
+
+    def test___eq__00(self):
+        self.assertEqual(td1, td2)
+
+    def test___eq__01(self):
+        self.assertTrue(not td1 != td2)
+
+    def test___eq__02(self):
+        self.assertEqual(timedelta(hours=6, minutes=60), a)
+
+    def test___eq__03(self):
+        self.assertEqual(timedelta(seconds=60 * 6), b)
+
+    def test___eq__04(self):
+        self.assertTrue(not td1 == td3)
+
+    def test___eq__05(self):
+        self.assertTrue(td1 != td3)
+
+    def test___eq__06(self):
+        self.assertTrue(td3 != td1)
+
+    def test___eq__07(self):
+        self.assertTrue(not td3 == td1)
+
+    def test___le__00(self):
+        self.assertTrue(td1 <= td2)
+
+    def test___le__01(self):
+        self.assertTrue(td1 <= td3)
+
+    def test___le__02(self):
+        self.assertTrue(not td3 <= td1)
+
+    def test___lt__00(self):
+        self.assertTrue(not td1 < td2)
+
+    def test___lt__01(self):
+        self.assertTrue(td1 < td3)
+
+    def test___lt__02(self):
+        self.assertTrue(not td3 < td1)
+
+    def test___ge__00(self):
+        self.assertTrue(td1 >= td2)
+
+    def test___ge__01(self):
+        self.assertTrue(td3 >= td1)
+
+    def test___ge__02(self):
+        self.assertTrue(not td1 >= td3)
+
+    def test___gt__00(self):
+        self.assertTrue(not td1 > td2)
+
+    def test___gt__01(self):
+        self.assertTrue(td3 > td1)
+
+    def test___gt__02(self):
+        self.assertTrue(not td1 > td3)
+
+    def test___bool__00(self):
+        self.assertTrue(timedelta(hours=1))
+
+    def test___bool__01(self):
+        self.assertTrue(timedelta(minutes=1))
+
+    def test___bool__02(self):
+        self.assertTrue(timedelta(seconds=1))
+
+    def test___bool__03(self):
+        self.assertTrue(not td0)
+
+    def test___str__00(self):
+        self.assertEqual(str(timedelta(days=1)), "1 day, 0:00:00")
+
+    def test___str__01(self):
+        self.assertEqual(str(timedelta(days=-1)), "-1 day, 0:00:00")
+
+    def test___str__02(self):
+        self.assertEqual(str(timedelta(days=2)), "2 days, 0:00:00")
+
+    def test___str__03(self):
+        self.assertEqual(str(timedelta(days=-2)), "-2 days, 0:00:00")
+
+    def test___str__04(self):
+        self.assertEqual(str(timedelta(hours=12, minutes=58, seconds=59)), "12:58:59")
+
+    def test___str__05(self):
+        self.assertEqual(str(timedelta(hours=2, minutes=3, seconds=4)), "2:03:04")
+
+    def test___hash__00(self):
+        self.assertEqual(td0, td4)
+        self.assertEqual(hash(td0), hash(td4))
+
+    def test___hash__01(self):
+        tt0 = td0 + timedelta(weeks=7)
+        tt4 = td4 + timedelta(days=7 * 7)
+        self.assertEqual(hash(tt0), hash(tt4))
+
+    def test___hash__02(self):
+        d = {td0: 1}
+        d[td4] = 2
+        self.assertEqual(len(d), 1)
+        self.assertEqual(d[td0], 2)
+
+    def test_constant00(self):
+        self.assertIsInstance(timedelta.min, timedelta)
+        self.assertIsInstance(timedelta.max, timedelta)
+        self.assertIsInstance(timedelta.resolution, timedelta)
+        self.assertTrue(timedelta.max > timedelta.min)
+
+    def test_constant01(self):
+        self.assertEqual(timedelta.min, timedelta(days=-999_999_999))
+
+    def test_constant02(self):
+        self.assertEqual(
+            timedelta.max,
+            timedelta(days=999_999_999, seconds=24 * 3600 - 1, microseconds=10**6 - 1),
+        )
+
+    def test_constant03(self):
+        self.assertEqual(timedelta.resolution, timedelta(microseconds=1))
+
+    def test_computation00(self):
+        self.assertEqual((3 * us) * 0.5, 2 * us)
+
+    def test_computation01(self):
+        self.assertEqual((5 * us) * 0.5, 2 * us)
+
+    def test_computation02(self):
+        self.assertEqual(0.5 * (3 * us), 2 * us)
+
+    def test_computation03(self):
+        self.assertEqual(0.5 * (5 * us), 2 * us)
+
+    def test_computation04(self):
+        self.assertEqual((-3 * us) * 0.5, -2 * us)
+
+    def test_computation05(self):
+        self.assertEqual((-5 * us) * 0.5, -2 * us)
+
+    def test_computation06(self):
+        self.assertEqual((3 * us) / 2, 2 * us)
+
+    def test_computation07(self):
+        self.assertEqual((5 * us) / 2, 2 * us)
+
+    def test_computation08(self):
+        self.assertEqual((-3 * us) / 2.0, -2 * us)
+
+    def test_computation09(self):
+        self.assertEqual((-5 * us) / 2.0, -2 * us)
+
+    def test_computation10(self):
+        self.assertEqual((3 * us) / -2, -2 * us)
+
+    def test_computation11(self):
+        self.assertEqual((5 * us) / -2, -2 * us)
+
+    def test_computation12(self):
+        self.assertEqual((3 * us) / -2.0, -2 * us)
+
+    def test_computation13(self):
+        self.assertEqual((5 * us) / -2.0, -2 * us)
+
+    def test_computation14(self):
+        for i in range(-10, 10):
+            # with self.subTest(i=i): not supported by Micropython
+            self.assertEqual((i * us / 3) // us, round(i / 3))
+
+    def test_computation15(self):
+        for i in range(-10, 10):
+            # with self.subTest(i=i): not supported by Micropython
+            self.assertEqual((i * us / -3) // us, round(i / -3))
+
+    def test_carries00(self):
+        td1 = timedelta(
+            days=100,
+            weeks=-7,
+            hours=-24 * (100 - 49),
+            minutes=-3,
+            seconds=3 * 60 + 1,
+        )
+        td2 = timedelta(seconds=1)
+        self.assertEqual(td1, td2)
+
+    def test_resolution00(self):
+        self.assertIsInstance(timedelta.min, timedelta)
+
+    def test_resolution01(self):
+        self.assertIsInstance(timedelta.max, timedelta)
+
+    def test_resolution02(self):
+        self.assertIsInstance(timedelta.resolution, timedelta)
+
+    def test_resolution03(self):
+        self.assertTrue(timedelta.max > timedelta.min)
+
+    def test_resolution04(self):
+        self.assertEqual(timedelta.resolution, timedelta(microseconds=1))
+
+    @unittest.skipIf(STDLIB, "standard timedelta has no tuple()")
+    def test_tuple00(self):
+        self.assertEqual(td1.tuple(), (2, 0, 0, 3, 4))
+
+    @unittest.skipIf(STDLIB, "standard timedelta has no tuple()")
+    def test_tuple01(self):
+        self.assertEqual(td1h2m40s100us.tuple(), (0, 1, 2, 40, 100))
+
+
+### timezone #################################################################
+
+
+class Cet(tzinfo):
+    # Central European Time (see https://en.wikipedia.org/wiki/Summer_time_in_Europe)
+
+    def utcoffset(self, dt):
+        h = 2 if self.isdst(dt)[0] else 1
+        return timedelta(hours=h)
+
+    def dst(self, dt):
+        h = 1 if self.isdst(dt)[0] else 0
+        return timedelta(hours=h)
+
+    def tzname(self, dt):
+        return "CEST" if self.isdst(dt)[0] else "CET"
+
+    def fromutc(self, dt):
+        assert dt.tzinfo is self
+        isdst, fold = self.isdst(dt, utc=True)
+        h = 2 if isdst else 1
+        dt += timedelta(hours=h)
+        dt = dt.replace(fold=fold)
+        return dt
+
+    def isdst(self, dt, utc=False):
+        if dt is None:
+            return False, None
+
+        year = dt.year
+        if not 2000 <= year < 2100:
+            # Formulas below are valid in the range [2000; 2100)
+            raise ValueError
+
+        hour = 1 if utc else 3
+        day = 31 - (5 * year // 4 + 4) % 7  # last Sunday of March
+        beg = datetime(year, 3, day, hour)
+        day = 31 - (5 * year // 4 + 1) % 7  # last Sunday of October
+        end = datetime(year, 10, day, hour)
+
+        dt = dt.replace(tzinfo=None)
+        if utc:
+            fold = 1 if end <= dt < end + timedelta(hours=1) else 0
+        else:
+            fold = dt.fold
+        isdst = beg <= dt < end
+        return isdst, fold
+
+    def __repr__(self):
+        return "Cet()"
+
+    def __str__(self):
+        return self.tzname(None)
+
+    def __eq__(self, other):
+        return repr(self) == repr(other)
+
+    def __hash__(self):
+        return hash(repr(self))
+
+
+class USTimeZone(tzinfo):
+    DSTSTART = datetime(1, 3, 8, 2)
+    DSTEND = datetime(1, 11, 1, 2)
+    ZERO = timedelta(0)
+    HOUR = timedelta(hours=1)
+
+    def __init__(self, hours, reprname, stdname, dstname):
+        self.stdoffset = timedelta(hours=hours)
+        self.reprname = reprname
+        self.stdname = stdname
+        self.dstname = dstname
+
+    def __repr__(self):
+        return self.reprname
+
+    def tzname(self, dt):
+        if self.dst(dt):
+            return self.dstname
+        else:
+            return self.stdname
+
+    def utcoffset(self, dt):
+        return self.stdoffset + self.dst(dt)
+
+    def dst(self, dt):
+        if dt is None or dt.tzinfo is None:
+            return self.ZERO
+        assert dt.tzinfo is self
+        start, end = USTimeZone.us_dst_range(dt.year)
+        dt = dt.replace(tzinfo=None)
+        if start + self.HOUR <= dt < end - self.HOUR:
+            return self.HOUR
+        if end - self.HOUR <= dt < end:
+            return self.ZERO if dt.fold else self.HOUR
+        if start <= dt < start + self.HOUR:
+            return self.HOUR if dt.fold else self.ZERO
+        return self.ZERO
+
+    def fromutc(self, dt):
+        assert dt.tzinfo is self
+        start, end = USTimeZone.us_dst_range(dt.year)
+        start = start.replace(tzinfo=self)
+        end = end.replace(tzinfo=self)
+        std_time = dt + self.stdoffset
+        dst_time = std_time + self.HOUR
+        if end <= dst_time < end + self.HOUR:
+            return std_time.replace(fold=1)
+        if std_time < start or dst_time >= end:
+            return std_time
+        if start <= std_time < end - self.HOUR:
+            return dst_time
+
+    @staticmethod
+    def us_dst_range(year):
+        start = first_sunday_on_or_after(USTimeZone.DSTSTART.replace(year=year))
+        end = first_sunday_on_or_after(USTimeZone.DSTEND.replace(year=year))
+        return start, end
+
+    @staticmethod
+    def first_sunday_on_or_after(dt):
+        days_to_go = 6 - dt.weekday()
+        if days_to_go:
+            dt += timedelta(days_to_go)
+        return dt
+
+
+class LocalTz:
+    def __init__(self, tz):
+        self.tz = tz
+        self._old = None
+
+    @staticmethod
+    def _set(tz):
+        if hasattr(mod_time, "tzset"):  # Python
+            if tz:
+                os.environ["TZ"] = tz
+            else:
+                del os.environ["TZ"]
+            mod_time.tzset()
+        else:
+            if tz:
+                os.putenv("TZ", tz)
+            else:
+                os.unsetenv("TZ")
+
+    def set(self):
+        self._old = os.getenv("TZ")
+        LocalTz._set(self.tz)
+
+    def unset(self):
+        LocalTz._set(self._old)
+        self._old = None
+
+    def __enter__(self):
+        self.set()
+
+    def __exit__(self, typ, value, trace):
+        self.unset()
+
+
+tz_acdt = timezone(timedelta(hours=9.5), "ACDT")
+tz_est = timezone(-timedelta(hours=5), "EST")
+tz1 = timezone(timedelta(hours=-1))
+tz2 = Cet()
+tz3 = USTimeZone(-5, "Eastern", "EST", "EDT")
+
+
+class Test1TimeZone(unittest.TestCase):
+    def test___init__00(self):
+        self.assertEqual(str(tz_acdt), "ACDT")
+        self.assertEqual(str(tz_acdt), tz_acdt.tzname(None))
+
+    def test___init__01(self):
+        self.assertEqual(str(tz_est), "EST")
+        self.assertEqual(str(tz_est), tz_est.tzname(None))
+
+    def test___init__02(self):
+        self.assertEqual(str(tz1), "UTC-01:00")
+        self.assertEqual(str(tz1), tz1.tzname(None))
+
+    def test___init__03(self):
+        self.assertEqual(str(tz2), "CET")
+        self.assertEqual(str(tz2), tz2.tzname(None))
+
+    def test___init__04(self):
+        offset = timedelta(hours=-24, microseconds=1)
+        tz = timezone(offset)
+        self.assertIsInstance(tz, timezone)
+
+    def test___init__05(self):
+        offset = timedelta(hours=24, microseconds=-1)
+        tz = timezone(offset)
+        self.assertIsInstance(tz, timezone)
+
+    def test___init__06(self):
+        offset = timedelta(hours=-24)
+        self.assertRaises(ValueError, timezone, offset)
+
+    def test___init__07(self):
+        offset = timedelta(hours=24)
+        self.assertRaises(ValueError, timezone, offset)
+
+    def test___repr__00(self):
+        self.assertEqual(tz1, eval_mod(repr(tz1)))
+
+    def test___eq__00(self):
+        self.assertEqual(timezone(timedelta(hours=1)), timezone(timedelta(hours=1)))
+
+    def test___eq__01(self):
+        self.assertNotEqual(timezone(timedelta(hours=1)), timezone(timedelta(hours=2)))
+
+    def test___eq__02(self):
+        self.assertEqual(timezone(timedelta(hours=-5)), timezone(timedelta(hours=-5), "EST"))
+
+    def test_utcoffset00(self):
+        self.assertEqual(str(tz2.utcoffset(None)), "1:00:00")
+
+    def test_utcoffset01(self):
+        self.assertEqual(str(tz2.utcoffset(datetime(2010, 3, 27, 12))), "1:00:00")
+
+    def test_utcoffset02(self):
+        self.assertEqual(str(tz2.utcoffset(datetime(2010, 3, 28, 12))), "2:00:00")
+
+    def test_utcoffset03(self):
+        self.assertEqual(str(tz2.utcoffset(datetime(2010, 10, 30, 12))), "2:00:00")
+
+    def test_utcoffset04(self):
+        self.assertEqual(str(tz2.utcoffset(datetime(2010, 10, 31, 12))), "1:00:00")
+
+    def test_tzname00(self):
+        self.assertEqual(tz2.tzname(datetime(2011, 1, 1)), "CET")
+
+    def test_tzname01(self):
+        self.assertEqual(tz2.tzname(datetime(2011, 8, 1)), "CEST")
+
+    def test_utc00(self):
+        self.assertEqual(timezone.utc.utcoffset(None), td0)
+
+    def test_fromutc00(self):
+        utc = EPOCH.replace(tzinfo=tz_acdt)
+        self.assertEqual(tz_acdt.fromutc(utc), utc + 9.5 * td1h)
+
+    def test_fromutc01(self):
+        utc = EPOCH.replace(tzinfo=tz_est)
+        self.assertEqual(tz_est.fromutc(utc), utc + 5 * -td1h)
+
+    def test_fromutc02(self):
+        utc = datetime(2010, 3, 28, 0, 59, 59, 999_999, tz2)
+        dt = tz2.fromutc(utc)
+        self.assertEqual(dt, utc + td1h)
+        self.assertFalse(dt.fold)
+
+    def test_fromutc03(self):
+        utc = datetime(2010, 3, 28, 1, 0, 0, 0, tz2)
+        dt = tz2.fromutc(utc)
+        self.assertEqual(dt, utc + 2 * td1h)
+        self.assertFalse(dt.fold)
+
+    def test_fromutc04(self):
+        utc = datetime(2010, 10, 31, 0, 59, 59, 999_999, tz2)
+        dt = tz2.fromutc(utc)
+        self.assertEqual(dt, utc + 2 * td1h)
+        self.assertFalse(dt.fold)
+
+    def test_fromutc05(self):
+        utc = datetime(2010, 10, 31, 1, 0, 0, 0, tz2)
+        dt = tz2.fromutc(utc)
+        self.assertEqual(dt, utc + td1h)
+        self.assertTrue(dt.fold)
+
+    def test_fromutc06(self):
+        dt1 = tz2.fromutc(datetime(2010, 10, 31, 0, 0, 0, 0, tz2))
+        dt2 = tz2.fromutc(datetime(2010, 10, 31, 1, 0, 0, 0, tz2))
+        self.assertEqual(dt1, dt2)
+        self.assertNotEqual(dt1.fold, dt2.fold)
+
+    def test_aware_datetime00(self):
+        t = datetime(1, 1, 1)
+        self.assertEqual(tz1.tzname(t), t.replace(tzinfo=tz1).tzname())
+
+    def test_aware_datetime01(self):
+        t = datetime(1, 1, 1)
+        self.assertEqual(tz1.utcoffset(t), t.replace(tzinfo=tz1).utcoffset())
+
+    def test_aware_datetime02(self):
+        t = datetime(1, 1, 1)
+        self.assertEqual(tz1.dst(t), t.replace(tzinfo=tz1).dst())
+
+    def test_offset_boundaries00(self):
+        td = timedelta(hours=23, minutes=59, seconds=59, microseconds=999999)
+        for i in (1, -1):
+            self.assertIsInstance(timezone(i * td), timezone)
+
+    def test_offset_boundaries01(self):
+        td = timedelta(hours=24)
+        for i in (1, -1):
+            with self.assertRaises(ValueError):
+                timezone(i * td)
+
+
+### date #####################################################################
+
+d1 = date(2002, 1, 31)
+d1r = "datetime.date(0, 0, 730881)"
+d2 = date(1956, 1, 31)
+d2d1s = (46 * 365 + len(range(1956, 2002, 4))) * 24 * 60 * 60
+d3 = date(2002, 3, 1)
+d4 = date(2002, 3, 2)
+d5 = date(2002, 1, 31)
+
+hour = timedelta(hours=1)
+day = timedelta(days=1)
+week = timedelta(weeks=1)
+max_days = MAXYEAR * 365 + MAXYEAR // 4 - MAXYEAR // 100 + MAXYEAR // 400
+
+
+class Test2Date(unittest.TestCase):
+    def test___init__00(self):
+        self.assertEqual(d1.year, 2002)
+        self.assertEqual(d1.month, 1)
+        self.assertEqual(d1.day, 31)
+
+    @unittest.skipIf(STDLIB, "not supported by standard datetime")
+    def test___init__01(self):
+        date(0, 0, 1)
+
+    @unittest.skipIf(STDLIB, "not supported by standard datetime")
+    def test___init__02(self):
+        date(0, 0, max_days)
+
+    def test___init__03(self):
+        datetime(2000, 2, 29)
+
+    def test___init__04(self):
+        datetime(2004, 2, 29)
+
+    def test___init__05(self):
+        datetime(2400, 2, 29)
+
+    def test___init__06(self):
+        self.assertRaises(ValueError, datetime, 2000, 2, 30)
+
+    def test___init__07(self):
+        self.assertRaises(ValueError, datetime, 2001, 2, 29)
+
+    def test___init__08(self):
+        self.assertRaises(ValueError, datetime, 2100, 2, 29)
+
+    def test___init__09(self):
+        self.assertRaises(ValueError, datetime, 1900, 2, 29)
+
+    def test___init__10(self):
+        self.assertRaises(ValueError, date, MINYEAR - 1, 1, 1)
+        self.assertRaises(ValueError, date, MINYEAR, 0, 1)
+        self.assertRaises(ValueError, date, MINYEAR, 1, 0)
+
+    def test___init__11(self):
+        self.assertRaises(ValueError, date, MAXYEAR + 1, 12, 31)
+        self.assertRaises(ValueError, date, MAXYEAR, 13, 31)
+        self.assertRaises(ValueError, date, MAXYEAR, 12, 32)
+
+    def test___init__12(self):
+        self.assertRaises(ValueError, date, 1, 2, 29)
+        self.assertRaises(ValueError, date, 1, 4, 31)
+        self.assertRaises(ValueError, date, 1, 6, 31)
+        self.assertRaises(ValueError, date, 1, 9, 31)
+        self.assertRaises(ValueError, date, 1, 11, 31)
+
+    def test_fromtimestamp00(self):
+        with LocalTz("UTC"):
+            d = date.fromtimestamp(1012435200)
+            self.assertEqual(d, d1)
+
+    def test_fromtimestamp01(self):
+        with LocalTz("UTC"):
+            d = date.fromtimestamp(1012435200 + 1)
+            self.assertEqual(d, d1)
+
+    def test_fromtimestamp02(self):
+        with LocalTz("UTC"):
+            d = date.fromtimestamp(1012435200 - 1)
+            self.assertEqual(d, d1 - timedelta(days=1))
+
+    def test_fromtimestamp03(self):
+        with LocalTz("Europe/Rome"):
+            d = date.fromtimestamp(1012435200 - 3601)
+            self.assertEqual(d, d1 - timedelta(days=1))
+
+    def test_today00(self):
+        tm = mod_time.localtime()[:3]
+        dt = date.today()
+        dd = (dt.year, dt.month, dt.day)
+        self.assertEqual(tm, dd)
+
+    def test_fromordinal00(self):
+        self.assertEqual(date.fromordinal(1), date(1, 1, 1))
+
+    def test_fromordinal01(self):
+        self.assertEqual(date.fromordinal(max_days), date(MAXYEAR, 12, 31))
+
+    def test_fromisoformat00(self):
+        self.assertEqual(datetime.fromisoformat("1975-08-10"), datetime(1975, 8, 10))
+
+    def test_year00(self):
+        self.assertEqual(d1.year, 2002)
+
+    def test_year01(self):
+        self.assertEqual(d2.year, 1956)
+
+    def test_month00(self):
+        self.assertEqual(d1.month, 1)
+
+    def test_month01(self):
+        self.assertEqual(d4.month, 3)
+
+    def test_day00(self):
+        self.assertEqual(d1.day, 31)
+
+    def test_day01(self):
+        self.assertEqual(d4.day, 2)
+
+    def test_toordinal00(self):
+        self.assertEqual(date(1, 1, 1).toordinal(), 1)
+
+    def test_toordinal01(self):
+        self.assertEqual(date(MAXYEAR, 12, 31).toordinal(), max_days)
+
+    def test_timetuple00(self):
+        self.assertEqual(d1.timetuple()[:8], (2002, 1, 31, 0, 0, 0, 3, 31))
+
+    def test_timetuple01(self):
+        self.assertEqual(d3.timetuple()[:8], (2002, 3, 1, 0, 0, 0, 4, 60))
+
+    def test_replace00(self):
+        self.assertEqual(d1.replace(), d1)
+
+    def test_replace01(self):
+        self.assertEqual(d1.replace(year=2001), date(2001, 1, 31))
+
+    def test_replace02(self):
+        self.assertEqual(d1.replace(month=5), date(2002, 5, 31))
+
+    def test_replace03(self):
+        self.assertEqual(d1.replace(day=16), date(2002, 1, 16))
+
+    def test___add__00(self):
+        self.assertEqual(d4 + hour, d4)
+
+    def test___add__01(self):
+        self.assertEqual(d4 + day, date(2002, 3, 3))
+
+    def test___add__02(self):
+        self.assertEqual(d4 + week, date(2002, 3, 9))
+
+    def test___add__03(self):
+        self.assertEqual(d4 + 52 * week, date(2003, 3, 1))
+
+    def test___add__04(self):
+        self.assertEqual(d4 + -hour, date(2002, 3, 1))
+
+    def test___add__05(self):
+        self.assertEqual(d5 + -day, date(2002, 1, 30))
+
+    def test___add__06(self):
+        self.assertEqual(d4 + -week, date(2002, 2, 23))
+
+    def test___sub__00(self):
+        d = d1 - d2
+        self.assertEqual(d.total_seconds(), d2d1s)
+
+    def test___sub__01(self):
+        self.assertEqual(d4 - hour, d4)
+
+    def test___sub__02(self):
+        self.assertEqual(d4 - day, date(2002, 3, 1))
+
+    def test___sub__03(self):
+        self.assertEqual(d4 - week, date(2002, 2, 23))
+
+    def test___sub__04(self):
+        self.assertEqual(d4 - 52 * week, date(2001, 3, 3))
+
+    def test___sub__05(self):
+        self.assertEqual(d4 - -hour, date(2002, 3, 3))
+
+    def test___sub__06(self):
+        self.assertEqual(d4 - -day, date(2002, 3, 3))
+
+    def test___sub__07(self):
+        self.assertEqual(d4 - -week, date(2002, 3, 9))
+
+    def test___eq__00(self):
+        self.assertEqual(d1, d5)
+
+    def test___eq__01(self):
+        self.assertFalse(d1 != d5)
+
+    def test___eq__02(self):
+        self.assertTrue(d2 != d5)
+
+    def test___eq__03(self):
+        self.assertTrue(d5 != d2)
+
+    def test___eq__04(self):
+        self.assertFalse(d2 == d5)
+
+    def test___eq__05(self):
+        self.assertFalse(d5 == d2)
+
+    def test___eq__06(self):
+        self.assertFalse(d1 == None)
+
+    def test___eq__07(self):
+        self.assertTrue(d1 != None)
+
+    def test___le__00(self):
+        self.assertTrue(d1 <= d5)
+
+    def test___le__01(self):
+        self.assertTrue(d2 <= d5)
+
+    def test___le__02(self):
+        self.assertFalse(d5 <= d2)
+
+    def test___ge__00(self):
+        self.assertTrue(d1 >= d5)
+
+    def test___ge__01(self):
+        self.assertTrue(d5 >= d2)
+
+    def test___ge__02(self):
+        self.assertFalse(d2 >= d5)
+
+    def test___lt__00(self):
+        self.assertFalse(d1 < d5)
+
+    def test___lt__01(self):
+        self.assertTrue(d2 < d5)
+
+    def test___lt__02(self):
+        self.assertFalse(d5 < d2)
+
+    def test___gt__00(self):
+        self.assertFalse(d1 > d5)
+
+    def test___gt__01(self):
+        self.assertTrue(d5 > d2)
+
+    def test___gt__02(self):
+        self.assertFalse(d2 > d5)
+
+    def test_weekday00(self):
+        for i in range(7):
+            # March 4, 2002 is a Monday
+            self.assertEqual(datetime(2002, 3, 4 + i).weekday(), i)
+            # January 2, 1956 is a Monday
+            self.assertEqual(datetime(1956, 1, 2 + i).weekday(), i)
+
+    def test_isoweekday00(self):
+        for i in range(7):
+            self.assertEqual(datetime(2002, 3, 4 + i).isoweekday(), i + 1)
+            self.assertEqual(datetime(1956, 1, 2 + i).isoweekday(), i + 1)
+
+    def test_isoformat00(self):
+        self.assertEqual(d1.isoformat(), "2002-01-31")
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__00(self):
+        self.assertEqual(repr(d1), d1r)
+
+    def test___repr__01(self):
+        self.assertEqual(d1, eval_mod(repr(d1)))
+
+    def test___hash__00(self):
+        self.assertEqual(d1, d5)
+        self.assertEqual(hash(d1), hash(d5))
+
+    def test___hash__01(self):
+        dd1 = d1 + timedelta(weeks=7)
+        dd5 = d5 + timedelta(days=7 * 7)
+        self.assertEqual(hash(dd1), hash(dd5))
+
+    def test___hash__02(self):
+        d = {d1: 1}
+        d[d5] = 2
+        self.assertEqual(len(d), 1)
+        self.assertEqual(d[d1], 2)
+
+
+### time #####################################################################
+
+t1 = time(18, 45, 3, 1234)
+t1r = "datetime.time(microsecond=67503001234, tzinfo=None, fold=0)"
+t1f = time(18, 45, 3, 1234, fold=1)
+t1fr = f"datetime.time(microsecond=67503001234, tzinfo=None, fold=1)"
+t1z = time(18, 45, 3, 1234, tz1)
+t1zr = f"datetime.time(microsecond=67503001234, tzinfo={repr(tz1)}, fold=0)"
+t2 = time(12, 59, 59, 100)
+t2z = time(12, 59, 59, 100, tz2)
+t3 = time(18, 45, 3, 1234)
+t3z = time(18, 45, 3, 1234, tz2)
+t4 = time(18, 45, 3, 1234, fold=1)
+t4z = time(18, 45, 3, 1234, tz2, fold=1)
+t5z = time(20, 45, 3, 1234, tz2)
+
+
+class Test3Time(unittest.TestCase):
+    def test___init__00(self):
+        t = time()
+        self.assertEqual(t.hour, 0)
+        self.assertEqual(t.minute, 0)
+        self.assertEqual(t.second, 0)
+        self.assertEqual(t.microsecond, 0)
+        self.assertEqual(t.tzinfo, None)
+        self.assertEqual(t.fold, 0)
+
+    def test___init__01(self):
+        t = time(12)
+        self.assertEqual(t.hour, 12)
+        self.assertEqual(t.minute, 0)
+        self.assertEqual(t.second, 0)
+        self.assertEqual(t.microsecond, 0)
+        self.assertEqual(t.tzinfo, None)
+        self.assertEqual(t.fold, 0)
+
+    def test___init__02(self):
+        self.assertEqual(t1z.hour, 18)
+        self.assertEqual(t1z.minute, 45)
+        self.assertEqual(t1z.second, 3)
+        self.assertEqual(t1z.microsecond, 1234)
+        self.assertEqual(t1z.tzinfo, tz1)
+        self.assertEqual(t1z.fold, 0)
+
+    def test___init__03(self):
+        t = time(microsecond=1, fold=1)
+        self.assertEqual(t.fold, 1)
+
+    @unittest.skipIf(STDLIB, "not supported by standard datetime")
+    def test___init__04(self):
+        time(microsecond=24 * 60 * 60 * 1_000_000 - 1)
+
+    def test___init__05(self):
+        self.assertRaises(ValueError, time, -1, 0, 0, 0)
+        self.assertRaises(ValueError, time, 0, -1, 0, 0)
+        self.assertRaises(ValueError, time, 0, 0, -1, 0)
+        self.assertRaises(ValueError, time, 0, 0, 0, -1)
+        self.assertRaises(ValueError, time, 0, 0, 0, 0, fold=-1)
+
+    def test___init__06(self):
+        self.assertRaises(ValueError, time, 24, 0, 0, 0)
+        self.assertRaises(ValueError, time, 0, 60, 0, 0)
+        self.assertRaises(ValueError, time, 0, 0, 60, 0)
+        self.assertRaises(ValueError, time, 0, 0, 0, 0, fold=2)
+
+    @unittest.skipIf(STDLIB, "not supported by standard datetime")
+    def test___init__07(self):
+        self.assertRaises(ValueError, time, microsecond=24 * 60 * 60 * 1_000_000)
+
+    def test_fromisoformat00(self):
+        self.assertEqual(time.fromisoformat("01"), time(1))
+
+    def test_fromisoformat01(self):
+        self.assertEqual(time.fromisoformat("13:30"), time(13, 30))
+
+    def test_fromisoformat02(self):
+        self.assertEqual(time.fromisoformat("23:30:12"), time(23, 30, 12))
+
+    def test_fromisoformat03(self):
+        self.assertEqual(str(time.fromisoformat("11:03:04+01:00")), "11:03:04+01:00")
+
+    def test_hour00(self):
+        self.assertEqual(t1.hour, 18)
+
+    def test_hour01(self):
+        self.assertEqual(t2z.hour, 12)
+
+    def test_minute00(self):
+        self.assertEqual(t1.minute, 45)
+
+    def test_minute01(self):
+        self.assertEqual(t2z.minute, 59)
+
+    def test_second00(self):
+        self.assertEqual(t1.second, 3)
+
+    def test_second01(self):
+        self.assertEqual(t2z.second, 59)
+
+    def test_microsecond00(self):
+        self.assertEqual(t1.microsecond, 1234)
+
+    def test_microsecond01(self):
+        self.assertEqual(t2z.microsecond, 100)
+
+    def test_tzinfo00(self):
+        self.assertEqual(t1.tzinfo, None)
+
+    def test_tzinfo01(self):
+        self.assertEqual(t2z.tzinfo, tz2)
+
+    def test_fold00(self):
+        self.assertEqual(t1.fold, 0)
+
+    def test_replace00(self):
+        self.assertEqual(t2z.replace(), t2z)
+
+    def test_replace01(self):
+        self.assertEqual(t2z.replace(hour=20), time(20, 59, 59, 100, tz2))
+
+    def test_replace02(self):
+        self.assertEqual(t2z.replace(minute=4), time(12, 4, 59, 100, tz2))
+
+    def test_replace03(self):
+        self.assertEqual(t2z.replace(second=16), time(12, 59, 16, 100, tz2))
+
+    def test_replace04(self):
+        self.assertEqual(t2z.replace(microsecond=99), time(12, 59, 59, 99, tz2))
+
+    def test_replace05(self):
+        self.assertEqual(t2z.replace(tzinfo=tz1), time(12, 59, 59, 100, tz1))
+
+    def test_isoformat00(self):
+        self.assertEqual(t1.isoformat(), "18:45:03.001234")
+
+    def test_isoformat01(self):
+        self.assertEqual(t1z.isoformat(), "18:45:03.001234-01:00")
+
+    def test_isoformat02(self):
+        self.assertEqual(t2z.isoformat(), "12:59:59.000100+01:00")
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__00(self):
+        self.assertEqual(repr(t1), t1r)
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__01(self):
+        self.assertEqual(repr(t1f), t1fr)
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__02(self):
+        self.assertEqual(repr(t1z), t1zr)
+
+    def test___repr__03(self):
+        self.assertEqual(t1, eval_mod(repr(t1)))
+
+    def test___repr__04(self):
+        self.assertEqual(t1z, eval_mod(repr(t1z)))
+
+    def test___repr__05(self):
+        self.assertEqual(t4, eval_mod(repr(t4)))
+
+    def test___repr__06(self):
+        dt = eval_mod(repr(t4z))
+        self.assertEqual(t4z, eval_mod(repr(t4z)))
+
+    def test___bool__00(self):
+        self.assertTrue(t1)
+
+    def test___bool__01(self):
+        self.assertTrue(t1z)
+
+    def test___bool__02(self):
+        self.assertTrue(time())
+
+    def test___eq__00(self):
+        self.assertEqual(t1, t1)
+
+    def test___eq__01(self):
+        self.assertEqual(t1z, t1z)
+
+    def test___eq__02(self):
+        self.assertNotEqual(t1, t1z)
+
+    def test___eq__03(self):
+        self.assertNotEqual(t1z, t2z)
+
+    def test___eq__04(self):
+        self.assertEqual(t1z, t5z)
+
+    def test___eq__05(self):
+        self.assertEqual(t1, t1f)
+
+    def test___lt__00(self):
+        self.assertTrue(t2 < t1)
+
+    def test___lt__01(self):
+        self.assertTrue(t2z < t1z)
+
+    def test___lt__02(self):
+        self.assertRaises(TypeError, t1.__lt__, t1z)
+
+    def test___le__00(self):
+        self.assertTrue(t3 <= t1)
+
+    def test___le__01(self):
+        self.assertTrue(t1z <= t5z)
+
+    def test___le__02(self):
+        self.assertRaises(TypeError, t1.__le__, t1z)
+
+    def test___ge__00(self):
+        self.assertTrue(t1 >= t3)
+
+    def test___ge__01(self):
+        self.assertTrue(t5z >= t1z)
+
+    def test___ge__02(self):
+        self.assertRaises(TypeError, t1.__ge__, t1z)
+
+    def test___gt__00(self):
+        self.assertTrue(t1 > t2)
+
+    def test___gt__01(self):
+        self.assertTrue(t1z > t2z)
+
+    def test___gt__02(self):
+        self.assertRaises(TypeError, t1.__gt__, t1z)
+
+    def test___hash__00(self):
+        self.assertEqual(t1, t3)
+        self.assertEqual(hash(t1), hash(t3))
+
+    def test___hash__01(self):
+        d = {t1: 1}
+        d[t3] = 3
+        self.assertEqual(len(d), 1)
+        self.assertEqual(d[t1], 3)
+
+    def test___hash__02(self):
+        self.assertNotEqual(t1, t1z)
+        self.assertNotEqual(hash(t1), hash(t1z))
+
+    def test___hash__03(self):
+        self.assertNotEqual(t1z, t3z)
+        self.assertNotEqual(hash(t1z), hash(t3z))
+
+    def test___hash__04(self):
+        tf = t1.replace(fold=1)
+        self.assertEqual(t1, tf)
+        self.assertEqual(hash(t1), hash(tf))
+
+    def test_utcoffset00(self):
+        self.assertEqual(t1.utcoffset(), None)
+
+    def test_utcoffset01(self):
+        self.assertEqual(t1z.utcoffset(), timedelta(hours=-1))
+
+    def test_utcoffset02(self):
+        self.assertEqual(t2z.utcoffset(), timedelta(hours=1))
+
+    def test_dst00(self):
+        self.assertEqual(t1.dst(), None)
+
+    def test_dst01(self):
+        self.assertEqual(t1z.dst(), None)
+
+    def test_dst02(self):
+        self.assertEqual(t2z.dst(), td0)
+
+    def test_tzname00(self):
+        self.assertEqual(t1.tzname(), None)
+
+    def test_tzname01(self):
+        self.assertEqual(t1z.tzname(), "UTC-01:00")
+
+    def test_tzname02(self):
+        self.assertEqual(t2z.tzname(), "CET")
+
+    def test_constant00(self):
+        self.assertIsInstance(timedelta.resolution, timedelta)
+        self.assertTrue(timedelta.max > timedelta.min)
+
+    def test_constant01(self):
+        self.assertEqual(time.min, time(0))
+
+    def test_constant02(self):
+        self.assertEqual(time.max, time(23, 59, 59, 999_999))
+
+    def test_constant03(self):
+        self.assertEqual(time.resolution, timedelta(microseconds=1))
+
+
+### datetime #################################################################
+
+dt1 = datetime(2002, 1, 31)
+dt1z1 = datetime(2002, 1, 31, tzinfo=tz1)
+dt1z2 = datetime(2002, 1, 31, tzinfo=tz2)
+dt2 = datetime(1956, 1, 31)
+dt3 = datetime(2002, 3, 1, 12, 59, 59, 100, tz2)
+dt4 = datetime(2002, 3, 2, 17, 6)
+dt5 = datetime(2002, 1, 31)
+dt5z2 = datetime(2002, 1, 31, tzinfo=tz2)
+
+dt1r = "datetime.datetime(2002, 1, 31, 0, 0, 0, 0, None, fold=0)"
+dt3r = "datetime.datetime(2002, 3, 1, 12, 59, 59, 100, Cet(), fold=0)"
+dt4r = "datetime.datetime(2002, 3, 2, 17, 6, 0, 0, None, fold=0)"
+
+d1t1 = datetime(2002, 1, 31, 18, 45, 3, 1234)
+d1t1f = datetime(2002, 1, 31, 18, 45, 3, 1234, fold=1)
+d1t1z = datetime(2002, 1, 31, 18, 45, 3, 1234, tz1)
+
+dt27tz2 = datetime(2010, 3, 27, 12, tzinfo=tz2)  # last CET day
+dt28tz2 = datetime(2010, 3, 28, 12, tzinfo=tz2)  # first CEST day
+dt30tz2 = datetime(2010, 10, 30, 12, tzinfo=tz2)  # last CEST day
+dt31tz2 = datetime(2010, 10, 31, 12, tzinfo=tz2)  # first CET day
+
+
+# Tests where datetime depens on date and time
+class Test4DateTime(unittest.TestCase):
+    def test_combine00(self):
+        dt = datetime.combine(d1, t1)
+        self.assertEqual(dt, d1t1)
+
+    def test_combine01(self):
+        dt = datetime.combine(d1, t1)
+        self.assertEqual(dt.date(), d1)
+
+    def test_combine02(self):
+        dt1 = datetime.combine(d1, t1)
+        dt2 = datetime.combine(dt1, t1)
+        self.assertEqual(dt1, dt2)
+
+    def test_combine03(self):
+        dt = datetime.combine(d1, t1)
+        self.assertEqual(dt.time(), t1)
+
+    def test_combine04(self):
+        dt = datetime.combine(d1, t1, tz1)
+        self.assertEqual(dt, d1t1z)
+
+    def test_combine05(self):
+        dt = datetime.combine(d1, t1z)
+        self.assertEqual(dt, d1t1z)
+
+    def test_combine06(self):
+        dt = datetime.combine(d1, t1f)
+        self.assertEqual(dt, d1t1f)
+
+    def test_date00(self):
+        self.assertEqual(d1t1.date(), d1)
+
+    def test_time00(self):
+        self.assertEqual(d1t1.time(), t1)
+
+    def test_time01(self):
+        self.assertNotEqual(d1t1z.time(), t1z)
+
+    def test_timetz00(self):
+        self.assertEqual(d1t1.timetz(), t1)
+
+    def test_timetz01(self):
+        self.assertEqual(d1t1z.timetz(), t1z)
+
+    def test_timetz02(self):
+        self.assertEqual(d1t1f.timetz(), t1f)
+
+
+# Tests where datetime is independent from date and time
+class Test5DateTime(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        for k in ("date", "time"):
+            del mod_datetime.__dict__[k]
+
+    def test___init__00(self):
+        d = datetime(2002, 3, 1, 12, 0, fold=1)
+        self.assertEqual(d.year, 2002)
+        self.assertEqual(d.month, 3)
+        self.assertEqual(d.day, 1)
+        self.assertEqual(d.hour, 12)
+        self.assertEqual(d.minute, 0)
+        self.assertEqual(d.second, 0)
+        self.assertEqual(d.microsecond, 0)
+        self.assertEqual(d.tzinfo, None)
+        self.assertEqual(d.fold, 1)
+
+    def test___init__01(self):
+        self.assertEqual(dt3.year, 2002)
+        self.assertEqual(dt3.month, 3)
+        self.assertEqual(dt3.day, 1)
+        self.assertEqual(dt3.hour, 12)
+        self.assertEqual(dt3.minute, 59)
+        self.assertEqual(dt3.second, 59)
+        self.assertEqual(dt3.microsecond, 100)
+        self.assertEqual(dt3.tzinfo, tz2)
+        self.assertEqual(dt3.fold, 0)
+
+    def test___init__02(self):
+        datetime(MINYEAR, 1, 1)
+
+    def test___init__03(self):
+        datetime(MAXYEAR, 12, 31)
+
+    def test___init__04(self):
+        self.assertRaises(ValueError, datetime, MINYEAR - 1, 1, 1)
+
+    def test___init__05(self):
+        self.assertRaises(ValueError, datetime, MAXYEAR + 1, 1, 1)
+
+    def test___init__06(self):
+        self.assertRaises(ValueError, datetime, 2000, 0, 1)
+
+    def test___init__07(self):
+        datetime(2000, 2, 29)
+
+    def test___init__08(self):
+        datetime(2004, 2, 29)
+
+    def test___init__09(self):
+        datetime(2400, 2, 29)
+
+    def test___init__10(self):
+        self.assertRaises(ValueError, datetime, 2000, 2, 30)
+
+    def test___init__11(self):
+        self.assertRaises(ValueError, datetime, 2001, 2, 29)
+
+    def test___init__12(self):
+        self.assertRaises(ValueError, datetime, 2100, 2, 29)
+
+    def test___init__13(self):
+        self.assertRaises(ValueError, datetime, 1900, 2, 29)
+
+    def test___init__14(self):
+        self.assertRaises(ValueError, datetime, 2000, 1, 0)
+
+    def test___init__15(self):
+        self.assertRaises(ValueError, datetime, 2000, 1, 32)
+
+    def test___init__16(self):
+        self.assertRaises(ValueError, datetime, 2000, 1, 31, -1)
+
+    def test___init__17(self):
+        self.assertRaises(ValueError, datetime, 2000, 1, 31, 24)
+
+    def test___init__18(self):
+        self.assertRaises(ValueError, datetime, 2000, 1, 31, 23, -1)
+
+    def test___init__19(self):
+        self.assertRaises(ValueError, datetime, 2000, 1, 31, 23, 60)
+
+    def test___init__20(self):
+        self.assertRaises(ValueError, datetime, 2000, 1, 31, 23, 59, -1)
+
+    def test___init__21(self):
+        self.assertRaises(ValueError, datetime, 2000, 1, 31, 23, 59, 60)
+
+    def test___init__22(self):
+        self.assertEqual(dt1, eval_mod(dt1r))
+
+    def test___init__23(self):
+        self.assertEqual(dt3, eval_mod(dt3r))
+
+    def test___init__24(self):
+        self.assertEqual(dt4, eval_mod(dt4r))
+
+    def test_fromtimestamp00(self):
+        with LocalTz("Europe/Rome"):
+            ts = 1012499103.001234
+            if LOCALTZ:
+                dt = datetime.fromtimestamp(ts)
+                self.assertEqual(dt, d1t1)
+            else:
+                self.assertRaises(NotImplementedError, datetime.fromtimestamp, ts)
+
+    def test_fromtimestamp01(self):
+        ts = 1012506303.001234
+        self.assertEqual(datetime.fromtimestamp(ts, tz1), d1t1z)
+
+    def test_fromtimestamp02(self):
+        ts = 1269687600
+        self.assertEqual(datetime.fromtimestamp(ts, tz2), dt27tz2)
+
+    def test_fromtimestamp03(self):
+        ts = 1269770400
+        self.assertEqual(datetime.fromtimestamp(ts, tz2), dt28tz2)
+
+    def test_fromtimestamp04(self):
+        with LocalTz("Europe/Rome"):
+            dt = datetime(2010, 10, 31, 0, 30, tzinfo=timezone.utc)
+            ts = (dt - EPOCH).total_seconds()
+            dt = dt.replace(tzinfo=None) + 2 * td1h
+            if LOCALTZ:
+                ds = datetime.fromtimestamp(ts)
+                self.assertEqual(ds, dt)
+                self.assertFalse(ds.fold)
+            else:
+                self.assertRaises(NotImplementedError, datetime.fromtimestamp, ts)
+
+    def test_fromtimestamp05(self):
+        with LocalTz("Europe/Rome"):
+            dt = datetime(2010, 10, 31, 1, 30, tzinfo=timezone.utc)
+            ts = (dt - EPOCH).total_seconds()
+            dt = dt.replace(tzinfo=None) + 1 * td1h
+            if LOCALTZ:
+                ds = datetime.fromtimestamp(ts)
+                self.assertEqual(ds, dt)
+                self.assertTrue(ds.fold)
+            else:
+                self.assertRaises(NotImplementedError, datetime.fromtimestamp, ts)
+
+    def test_fromtimestamp06(self):
+        with LocalTz("US/Eastern"):
+            dt = datetime(2020, 11, 1, 5, 30, tzinfo=timezone.utc)
+            ts = (dt - EPOCH).total_seconds()
+            dt = dt.replace(tzinfo=None) - 4 * td1h
+            if LOCALTZ:
+                ds = datetime.fromtimestamp(ts)
+                self.assertEqual(ds, dt)
+            else:
+                self.assertRaises(NotImplementedError, datetime.fromtimestamp, ts)
+
+    def test_fromtimestamp07(self):
+        with LocalTz("US/Eastern"):
+            dt = datetime(2020, 11, 1, 7, 30, tzinfo=timezone.utc)
+            ts = (dt - EPOCH).total_seconds()
+            dt = dt.replace(tzinfo=None) - 5 * td1h
+            if LOCALTZ:
+                ds = datetime.fromtimestamp(ts)
+                self.assertEqual(ds, dt)
+            else:
+                self.assertRaises(NotImplementedError, datetime.fromtimestamp, ts)
+
+    @unittest.skipIf(not LOCALTZ, "naive datetime not supported")
+    def test_now00(self):
+        tm = datetime(*mod_time.localtime()[:6])
+        dt = datetime.now()
+        self.assertAlmostEqual(tm, dt, delta=timedelta(seconds=1))
+
+    def test_now01(self):
+        tm = datetime(*mod_time.gmtime()[:6], tzinfo=tz2)
+        tm += tz2.utcoffset(tm)
+        dt = datetime.now(tz2)
+        self.assertAlmostEqual(tm, dt, delta=timedelta(seconds=1))
+
+    def test_fromordinal00(self):
+        self.assertEqual(datetime.fromordinal(1), datetime(1, 1, 1))
+
+    def test_fromordinal01(self):
+        self.assertEqual(datetime.fromordinal(max_days), datetime(MAXYEAR, 12, 31))
+
+    def test_fromisoformat00(self):
+        self.assertEqual(datetime.fromisoformat("1975-08-10"), datetime(1975, 8, 10))
+
+    def test_fromisoformat01(self):
+        self.assertEqual(datetime.fromisoformat("1975-08-10 23"), datetime(1975, 8, 10, 23))
+
+    def test_fromisoformat02(self):
+        self.assertEqual(datetime.fromisoformat("1975-08-10 23:30"), datetime(1975, 8, 10, 23, 30))
+
+    def test_fromisoformat03(self):
+        self.assertEqual(
+            datetime.fromisoformat("1975-08-10 23:30:12"), datetime(1975, 8, 10, 23, 30, 12)
+        )
+
+    def test_fromisoformat04(self):
+        self.assertEqual(
+            str(datetime.fromisoformat("1975-08-10 23:30:12+01:00")), "1975-08-10 23:30:12+01:00"
+        )
+
+    def test_year00(self):
+        self.assertEqual(dt1.year, 2002)
+
+    def test_year01(self):
+        self.assertEqual(dt2.year, 1956)
+
+    def test_month00(self):
+        self.assertEqual(dt1.month, 1)
+
+    def test_month01(self):
+        self.assertEqual(dt3.month, 3)
+
+    def test_day00(self):
+        self.assertEqual(dt1.day, 31)
+
+    def test_day01(self):
+        self.assertEqual(dt4.day, 2)
+
+    def test_hour00(self):
+        self.assertEqual(dt1.hour, 0)
+
+    def test_hour01(self):
+        self.assertEqual(dt3.hour, 12)
+
+    def test_minute00(self):
+        self.assertEqual(dt1.minute, 0)
+
+    def test_minute01(self):
+        self.assertEqual(dt3.minute, 59)
+
+    def test_second00(self):
+        self.assertEqual(dt1.second, 0)
+
+    def test_second01(self):
+        self.assertEqual(dt3.second, 59)
+
+    def test_microsecond00(self):
+        self.assertEqual(dt1.microsecond, 0)
+
+    def test_microsecond01(self):
+        self.assertEqual(dt3.microsecond, 100)
+
+    def test_tzinfo00(self):
+        self.assertEqual(dt1.tzinfo, None)
+
+    def test_tzinfo01(self):
+        self.assertEqual(dt3.tzinfo, tz2)
+
+    def test_fold00(self):
+        self.assertEqual(dt1.fold, 0)
+
+    def test___add__00(self):
+        self.assertEqual(dt4 + hour, datetime(2002, 3, 2, 18, 6))
+
+    def test___add__01(self):
+        self.assertEqual(hour + dt4, datetime(2002, 3, 2, 18, 6))
+
+    def test___add__02(self):
+        self.assertEqual(dt4 + 10 * hour, datetime(2002, 3, 3, 3, 6))
+
+    def test___add__03(self):
+        self.assertEqual(dt4 + day, datetime(2002, 3, 3, 17, 6))
+
+    def test___add__04(self):
+        self.assertEqual(dt4 + week, datetime(2002, 3, 9, 17, 6))
+
+    def test___add__05(self):
+        self.assertEqual(dt4 + 52 * week, datetime(2003, 3, 1, 17, 6))
+
+    def test___add__06(self):
+        self.assertEqual(dt4 + (week + day + hour), datetime(2002, 3, 10, 18, 6))
+
+    def test___add__07(self):
+        self.assertEqual(dt5 + -day, datetime(2002, 1, 30))
+
+    def test___add__08(self):
+        self.assertEqual(-hour + dt4, datetime(2002, 3, 2, 16, 6))
+
+    def test___sub__00(self):
+        d = dt1 - dt2
+        self.assertEqual(d.total_seconds(), d2d1s)
+
+    def test___sub__01(self):
+        self.assertEqual(dt4 - hour, datetime(2002, 3, 2, 16, 6))
+
+    def test___sub__02(self):
+        self.assertEqual(dt4 - hour, dt4 + -hour)
+
+    def test___sub__03(self):
+        self.assertEqual(dt4 - 20 * hour, datetime(2002, 3, 1, 21, 6))
+
+    def test___sub__04(self):
+        self.assertEqual(dt4 - day, datetime(2002, 3, 1, 17, 6))
+
+    def test___sub__05(self):
+        self.assertEqual(dt4 - week, datetime(2002, 2, 23, 17, 6))
+
+    def test___sub__06(self):
+        self.assertEqual(dt4 - 52 * week, datetime(2001, 3, 3, 17, 6))
+
+    def test___sub__07(self):
+        self.assertEqual(dt4 - (week + day + hour), datetime(2002, 2, 22, 16, 6))
+
+    def test_computation00(self):
+        self.assertEqual((dt4 + week) - dt4, week)
+
+    def test_computation01(self):
+        self.assertEqual((dt4 + day) - dt4, day)
+
+    def test_computation02(self):
+        self.assertEqual((dt4 + hour) - dt4, hour)
+
+    def test_computation03(self):
+        self.assertEqual(dt4 - (dt4 + week), -week)
+
+    def test_computation04(self):
+        self.assertEqual(dt4 - (dt4 + day), -day)
+
+    def test_computation05(self):
+        self.assertEqual(dt4 - (dt4 + hour), -hour)
+
+    def test_computation06(self):
+        self.assertEqual(dt4 - (dt4 - week), week)
+
+    def test_computation07(self):
+        self.assertEqual(dt4 - (dt4 - day), day)
+
+    def test_computation08(self):
+        self.assertEqual(dt4 - (dt4 - hour), hour)
+
+    def test_computation09(self):
+        self.assertEqual(dt4 + (week + day + hour), (((dt4 + week) + day) + hour))
+
+    def test_computation10(self):
+        self.assertEqual(dt4 - (week + day + hour), (((dt4 - week) - day) - hour))
+
+    def test___eq__00(self):
+        self.assertEqual(dt1, dt5)
+
+    def test___eq__01(self):
+        self.assertFalse(dt1 != dt5)
+
+    def test___eq__02(self):
+        self.assertTrue(dt2 != dt5)
+
+    def test___eq__03(self):
+        self.assertTrue(dt5 != dt2)
+
+    def test___eq__04(self):
+        self.assertFalse(dt2 == dt5)
+
+    def test___eq__05(self):
+        self.assertFalse(dt5 == dt2)
+
+    def test___eq__06(self):
+        self.assertFalse(dt1 == dt1z1)
+
+    def test___eq__07(self):
+        self.assertFalse(dt1z1 == dt1z2)
+
+    def test___eq__08(self):
+        self.assertTrue(dt1z2 == dt5z2)
+
+    def test___le__00(self):
+        self.assertTrue(dt1 <= dt5)
+
+    def test___le__01(self):
+        self.assertTrue(dt2 <= dt5)
+
+    def test___le__02(self):
+        self.assertFalse(dt5 <= dt2)
+
+    def test___le__03(self):
+        self.assertFalse(dt1z1 <= dt1z2)
+
+    def test___le__04(self):
+        self.assertTrue(dt1z2 <= dt5z2)
+
+    def test___le__05(self):
+        self.assertRaises(TypeError, dt1.__le__, dt1z1)
+
+    def test___ge__00(self):
+        self.assertTrue(dt1 >= dt5)
+
+    def test___ge__01(self):
+        self.assertTrue(dt5 >= dt2)
+
+    def test___ge__02(self):
+        self.assertFalse(dt2 >= dt5)
+
+    def test___ge__03(self):
+        self.assertTrue(dt1z1 >= dt1z2)
+
+    def test___ge__04(self):
+        self.assertTrue(dt1z2 >= dt5z2)
+
+    def test___ge__05(self):
+        self.assertRaises(TypeError, dt1.__ge__, dt1z1)
+
+    def test___lt__00(self):
+        self.assertFalse(dt1 < dt5)
+
+    def test___lt__01(self):
+        self.assertTrue(dt2 < dt5)
+
+    def test___lt__02(self):
+        self.assertFalse(dt5 < dt2)
+
+    def test___lt__03(self):
+        self.assertFalse(dt1z1 < dt1z2)
+
+    def test___lt__04(self):
+        self.assertFalse(dt1z2 < dt5z2)
+
+    def test___lt__05(self):
+        self.assertRaises(TypeError, dt1.__lt__, dt1z1)
+
+    def test___gt__00(self):
+        self.assertFalse(dt1 > dt5)
+
+    def test___gt__01(self):
+        self.assertTrue(dt5 > dt2)
+
+    def test___gt__02(self):
+        self.assertFalse(dt2 > dt5)
+
+    def test___gt__03(self):
+        self.assertTrue(dt1z1 > dt1z2)
+
+    def test___gt__04(self):
+        self.assertFalse(dt1z2 > dt5z2)
+
+    def test___gt__05(self):
+        self.assertRaises(TypeError, dt1.__gt__, dt1z1)
+
+    def test_replace00(self):
+        self.assertEqual(dt3.replace(), dt3)
+
+    def test_replace01(self):
+        self.assertEqual(dt3.replace(year=2001), datetime(2001, 3, 1, 12, 59, 59, 100, tz2))
+
+    def test_replace02(self):
+        self.assertEqual(dt3.replace(month=4), datetime(2002, 4, 1, 12, 59, 59, 100, tz2))
+
+    def test_replace03(self):
+        self.assertEqual(dt3.replace(day=16), datetime(2002, 3, 16, 12, 59, 59, 100, tz2))
+
+    def test_replace04(self):
+        self.assertEqual(dt3.replace(hour=13), datetime(2002, 3, 1, 13, 59, 59, 100, tz2))
+
+    def test_replace05(self):
+        self.assertEqual(dt3.replace(minute=0), datetime(2002, 3, 1, 12, 0, 59, 100, tz2))
+
+    def test_replace06(self):
+        self.assertEqual(dt3.replace(second=1), datetime(2002, 3, 1, 12, 59, 1, 100, tz2))
+
+    def test_replace07(self):
+        self.assertEqual(dt3.replace(microsecond=99), datetime(2002, 3, 1, 12, 59, 59, 99, tz2))
+
+    def test_replace08(self):
+        self.assertEqual(dt3.replace(tzinfo=tz1), datetime(2002, 3, 1, 12, 59, 59, 100, tz1))
+
+    def test_replace09(self):
+        self.assertRaises(ValueError, datetime(2000, 2, 29).replace, year=2001)
+
+    def test_astimezone00(self):
+        dt = datetime(2002, 3, 1, 11, 59, 59, 100, timezone.utc)
+        self.assertEqual(dt3.astimezone(timezone.utc), dt)
+
+    def test_astimezone01(self):
+        self.assertIs(dt1z1.astimezone(tz1), dt1z1)
+
+    def test_astimezone02(self):
+        dt = datetime(2002, 1, 31, 2, 0, tzinfo=tz2)
+        self.assertEqual(dt1z1.astimezone(tz2), dt)
+
+    def test_astimezone03(self):
+        dt = datetime(2002, 1, 31, 10, 30, tzinfo=tz_acdt)
+        self.assertEqual(dt1z1.astimezone(tz_acdt), dt)
+
+    def test_astimezone04(self):
+        with LocalTz("Europe/Rome"):
+            dt1 = dt27tz2
+            dt2 = dt1.replace(tzinfo=None)
+            if LOCALTZ:
+                self.assertEqual(dt1, dt2.astimezone(tz2))
+            else:
+                self.assertRaises(NotImplementedError, dt2.astimezone, tz2)
+
+    def test_astimezone05(self):
+        with LocalTz("Europe/Rome"):
+            dt1 = dt28tz2
+            dt2 = dt1.replace(tzinfo=None)
+            if LOCALTZ:
+                self.assertEqual(dt1, dt2.astimezone(tz2))
+            else:
+                self.assertRaises(NotImplementedError, dt2.astimezone, tz2)
+
+    def test_astimezone06(self):
+        with LocalTz("Europe/Rome"):
+            dt1 = dt30tz2
+            dt2 = dt1.replace(tzinfo=None)
+            if LOCALTZ:
+                self.assertEqual(dt1, dt2.astimezone(tz2))
+            else:
+                self.assertRaises(NotImplementedError, dt2.astimezone, tz2)
+
+    def test_astimezone07(self):
+        with LocalTz("Europe/Rome"):
+            dt1 = dt31tz2
+            dt2 = dt1.replace(tzinfo=None)
+            if LOCALTZ:
+                self.assertEqual(dt1, dt2.astimezone(tz2))
+            else:
+                self.assertRaises(NotImplementedError, dt2.astimezone, tz2)
+
+    def test_astimezone08(self):
+        with LocalTz("Europe/Rome"):
+            dt1 = dt3
+            dt2 = dt1.replace(tzinfo=None)
+            if LOCALTZ:
+                self.assertEqual(dt1, dt2.astimezone(tz2))
+            else:
+                self.assertRaises(NotImplementedError, dt2.astimezone, tz2)
+
+    def test_utcoffset00(self):
+        self.assertEqual(dt1.utcoffset(), None)
+
+    def test_utcoffset01(self):
+        self.assertEqual(dt27tz2.utcoffset(), timedelta(hours=1))
+
+    def test_utcoffset02(self):
+        self.assertEqual(dt28tz2.utcoffset(), timedelta(hours=2))
+
+    def test_utcoffset03(self):
+        self.assertEqual(dt30tz2.utcoffset(), timedelta(hours=2))
+
+    def test_utcoffset04(self):
+        self.assertEqual(dt31tz2.utcoffset(), timedelta(hours=1))
+
+    def test_dst00(self):
+        self.assertEqual(dt1.dst(), None)
+
+    def test_dst01(self):
+        self.assertEqual(dt27tz2.dst(), timedelta(hours=0))
+
+    def test_dst02(self):
+        self.assertEqual(dt28tz2.dst(), timedelta(hours=1))
+
+    def test_tzname00(self):
+        self.assertEqual(dt1.tzname(), None)
+
+    def test_tzname01(self):
+        self.assertEqual(dt27tz2.tzname(), "CET")
+
+    def test_tzname02(self):
+        self.assertEqual(dt28tz2.tzname(), "CEST")
+
+    def test_timetuple00(self):
+        with LocalTz("Europe/Rome"):
+            self.assertEqual(dt1.timetuple()[:8], (2002, 1, 31, 0, 0, 0, 3, 31))
+
+    @unittest.skip("broken when running with non-UTC timezone")
+    def test_timetuple01(self):
+        self.assertEqual(dt27tz2.timetuple()[:8], (2010, 3, 27, 12, 0, 0, 5, 86))
+
+    @unittest.skip("broken when running with non-UTC timezone")
+    def test_timetuple02(self):
+        self.assertEqual(dt28tz2.timetuple()[:8], (2010, 3, 28, 12, 0, 0, 6, 87))
+
+    def test_timetuple03(self):
+        with LocalTz("Europe/Rome"):
+            self.assertEqual(
+                dt27tz2.replace(tzinfo=None).timetuple()[:8], (2010, 3, 27, 12, 0, 0, 5, 86)
+            )
+
+    def test_timetuple04(self):
+        self.assertEqual(
+            dt28tz2.replace(tzinfo=None).timetuple()[:8], (2010, 3, 28, 12, 0, 0, 6, 87)
+        )
+
+    def test_toordinal00(self):
+        self.assertEqual(datetime(1, 1, 1).toordinal(), 1)
+
+    def test_toordinal01(self):
+        self.assertEqual(datetime(1, 12, 31).toordinal(), 365)
+
+    def test_toordinal02(self):
+        self.assertEqual(datetime(2, 1, 1).toordinal(), 366)
+
+    def test_toordinal03(self):
+        # https://www.timeanddate.com/date/dateadded.html?d1=1&m1=1&y1=1&type=add&ad=730882
+        self.assertEqual(dt1.toordinal(), 730_882 - 1)
+
+    def test_toordinal04(self):
+        # https://www.timeanddate.com/date/dateadded.html?d1=1&m1=1&y1=1&type=add&ad=730911
+        self.assertEqual(dt3.toordinal(), 730_911 - 1)
+
+    def test_weekday00(self):
+        self.assertEqual(dt1.weekday(), d1.weekday())
+
+    def test_timestamp00(self):
+        with LocalTz("Europe/Rome"):
+            if LOCALTZ:
+                self.assertEqual(d1t1.timestamp(), 1012499103.001234)
+            else:
+                self.assertRaises(NotImplementedError, d1t1.timestamp)
+
+    def test_timestamp01(self):
+        self.assertEqual(d1t1z.timestamp(), 1012506303.001234)
+
+    def test_timestamp02(self):
+        with LocalTz("Europe/Rome"):
+            dt = datetime(2010, 3, 28, 2, 30)  # doens't exist
+            if LOCALTZ:
+                self.assertEqual(dt.timestamp(), 1269739800.0)
+            else:
+                self.assertRaises(NotImplementedError, dt.timestamp)
+
+    def test_timestamp03(self):
+        with LocalTz("Europe/Rome"):
+            dt = datetime(2010, 8, 10, 2, 30)
+            if LOCALTZ:
+                self.assertEqual(dt.timestamp(), 1281400200.0)
+            else:
+                self.assertRaises(NotImplementedError, dt.timestamp)
+
+    def test_timestamp04(self):
+        with LocalTz("Europe/Rome"):
+            dt = datetime(2010, 10, 31, 2, 30, fold=0)
+            if LOCALTZ:
+                self.assertEqual(dt.timestamp(), 1288485000.0)
+            else:
+                self.assertRaises(NotImplementedError, dt.timestamp)
+
+    def test_timestamp05(self):
+        with LocalTz("Europe/Rome"):
+            dt = datetime(2010, 10, 31, 2, 30, fold=1)
+            if LOCALTZ:
+                self.assertEqual(dt.timestamp(), 1288488600.0)
+            else:
+                self.assertRaises(NotImplementedError, dt.timestamp)
+
+    def test_timestamp06(self):
+        with LocalTz("US/Eastern"):
+            dt = datetime(2020, 3, 8, 2, 30)  # doens't exist
+            if LOCALTZ:
+                self.assertEqual(dt.timestamp(), 1583652600.0)
+            else:
+                self.assertRaises(NotImplementedError, dt.timestamp)
+
+    def test_timestamp07(self):
+        with LocalTz("US/Eastern"):
+            dt = datetime(2020, 8, 10, 2, 30)
+            if LOCALTZ:
+                self.assertEqual(dt.timestamp(), 1597041000.0)
+            else:
+                self.assertRaises(NotImplementedError, dt.timestamp)
+
+    def test_timestamp08(self):
+        with LocalTz("US/Eastern"):
+            dt = datetime(2020, 11, 1, 2, 30, fold=0)
+            if LOCALTZ:
+                self.assertEqual(dt.timestamp(), 1604215800.0)
+            else:
+                self.assertRaises(NotImplementedError, dt.timestamp)
+
+    def test_timestamp09(self):
+        with LocalTz("US/Eastern"):
+            dt = datetime(2020, 11, 1, 2, 30, fold=1)
+            if LOCALTZ:
+                self.assertEqual(dt.timestamp(), 1604215800.0)
+            else:
+                self.assertRaises(NotImplementedError, dt.timestamp)
+
+    def test_isoweekday00(self):
+        self.assertEqual(dt1.isoweekday(), d1.isoweekday())
+
+    def test_isoformat00(self):
+        self.assertEqual(dt3.isoformat(), "2002-03-01T12:59:59.000100+01:00")
+
+    def test_isoformat01(self):
+        self.assertEqual(dt3.isoformat("T"), "2002-03-01T12:59:59.000100+01:00")
+
+    def test_isoformat02(self):
+        self.assertEqual(dt3.isoformat(" "), "2002-03-01 12:59:59.000100+01:00")
+
+    def test_isoformat03(self):
+        self.assertEqual(str(dt3), "2002-03-01 12:59:59.000100+01:00")
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__00(self):
+        self.assertEqual(repr(dt1), dt1r)
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__01(self):
+        self.assertEqual(repr(dt3), dt3r)
+
+    @unittest.skipIf(STDLIB, "standard datetime differs")
+    def test___repr__02(self):
+        self.assertEqual(repr(dt4), dt4r)
+
+    def test___repr__03(self):
+        self.assertEqual(dt1, eval_mod(repr(dt1)))
+
+    def test___repr__04(self):
+        self.assertEqual(dt3, eval_mod(repr(dt3)))
+
+    def test___repr__05(self):
+        self.assertEqual(dt4, eval_mod(repr(dt4)))
+
+    def test___hash__00(self):
+        self.assertEqual(dt1, dt5)
+        self.assertEqual(hash(dt1), hash(dt5))
+
+    def test___hash__01(self):
+        dd1 = dt1 + timedelta(weeks=7)
+        dd5 = dt5 + timedelta(days=7 * 7)
+        self.assertEqual(hash(dd1), hash(dd5))
+
+    def test___hash__02(self):
+        d = {dt1: 1}
+        d[dt5] = 2
+        self.assertEqual(len(d), 1)
+        self.assertEqual(d[dt1], 2)
+
+    def test___hash__03(self):
+        self.assertNotEqual(dt1, dt1z1)
+        self.assertNotEqual(hash(dt1), hash(dt1z1))
+
+    def test___hash__04(self):
+        self.assertNotEqual(dt1z1, dt5z2)
+        self.assertNotEqual(hash(dt1z1), hash(dt5z2))
+
+    @unittest.skipIf(STDLIB, "standard datetime has no tuple()")
+    def test_tuple00(self):
+        self.assertEqual(dt1.tuple(), (2002, 1, 31, 0, 0, 0, 0, None, 0))
+
+    @unittest.skipIf(STDLIB, "standard datetime has no tuple()")
+    def test_tuple01(self):
+        self.assertEqual(dt27tz2.tuple(), (2010, 3, 27, 12, 0, 0, 0, tz2, 0))
+
+    @unittest.skipIf(STDLIB, "standard datetime has no tuple()")
+    def test_tuple02(self):
+        self.assertEqual(dt28tz2.tuple(), (2010, 3, 28, 12, 0, 0, 0, tz2, 0))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python-stdlib/errno/errno.py b/python-stdlib/errno/errno.py
new file mode 100644
index 000000000..c513a7f14
--- /dev/null
+++ b/python-stdlib/errno/errno.py
@@ -0,0 +1,39 @@
+EPERM = 1  # Operation not permitted
+ENOENT = 2  # No such file or directory
+ESRCH = 3  # No such process
+EINTR = 4  # Interrupted system call
+EIO = 5  # I/O error
+ENXIO = 6  # No such device or address
+E2BIG = 7  # Argument list too long
+ENOEXEC = 8  # Exec format error
+EBADF = 9  # Bad file number
+ECHILD = 10  # No child processes
+EAGAIN = 11  # Try again
+ENOMEM = 12  # Out of memory
+EACCES = 13  # Permission denied
+EFAULT = 14  # Bad address
+ENOTBLK = 15  # Block device required
+EBUSY = 16  # Device or resource busy
+EEXIST = 17  # File exists
+EXDEV = 18  # Cross-device link
+ENODEV = 19  # No such device
+ENOTDIR = 20  # Not a directory
+EISDIR = 21  # Is a directory
+EINVAL = 22  # Invalid argument
+ENFILE = 23  # File table overflow
+EMFILE = 24  # Too many open files
+ENOTTY = 25  # Not a typewriter
+ETXTBSY = 26  # Text file busy
+EFBIG = 27  # File too large
+ENOSPC = 28  # No space left on device
+ESPIPE = 29  # Illegal seek
+EROFS = 30  # Read-only file system
+EMLINK = 31  # Too many links
+EPIPE = 32  # Broken pipe
+EDOM = 33  # Math argument out of domain of func
+ERANGE = 34  # Math result not representable
+EAFNOSUPPORT = 97  # Address family not supported by protocol
+ECONNRESET = 104  # Connection timed out
+ENOTCONN = 107 # Not connected
+ETIMEDOUT = 110  # Connection timed out
+EINPROGRESS = 115  # Operation now in progress
diff --git a/python-stdlib/errno/manifest.py b/python-stdlib/errno/manifest.py
new file mode 100644
index 000000000..075d3403d
--- /dev/null
+++ b/python-stdlib/errno/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.2.0")
+
+module("errno.py")
diff --git a/python-stdlib/fnmatch/fnmatch.py b/python-stdlib/fnmatch/fnmatch.py
new file mode 100644
index 000000000..2b42c3be4
--- /dev/null
+++ b/python-stdlib/fnmatch/fnmatch.py
@@ -0,0 +1,139 @@
+"""Filename matching with shell patterns.
+
+fnmatch(FILENAME, PATTERN) matches according to the local convention.
+fnmatchcase(FILENAME, PATTERN) always takes case in account.
+
+The functions operate by translating the pattern into a regular
+expression.  They cache the compiled regular expressions for speed.
+
+The function translate(PATTERN) returns a regular expression
+corresponding to PATTERN.  (It does not compile it.)
+"""
+import re
+
+try:
+    from os.path import normcase
+except ImportError:
+
+    def normcase(s):
+        """
+        From os.path.normcase
+        Normalize the case of a pathname. On Windows, convert all characters
+        in the pathname to lowercase, and also convert forward slashes to
+        backward slashes. On other operating systems, return the path unchanged.
+        """
+        return s
+
+
+__all__ = ["filter", "fnmatch", "fnmatchcase", "translate"]
+
+
+def fnmatch(name, pat):
+    """Test whether FILENAME matches PATTERN.
+
+    Patterns are Unix shell style:
+
+    *       matches everything
+    ?       matches any single character
+    [seq]   matches any character in seq
+    [!seq]  matches any char not in seq
+
+    An initial period in FILENAME is not special.
+    Both FILENAME and PATTERN are first case-normalized
+    if the operating system requires it.
+    If you don't want this, use fnmatchcase(FILENAME, PATTERN).
+    """
+    name = normcase(name)
+    pat = normcase(pat)
+    return fnmatchcase(name, pat)
+
+
+# @functools.lru_cache(maxsize=256, typed=True)
+def _compile_pattern(pat):
+    if isinstance(pat, bytes):
+        pat_str = str(pat, "ISO-8859-1")
+        res_str = translate(pat_str)
+        res = bytes(res_str, "ISO-8859-1")
+    else:
+        res = translate(pat)
+
+    try:
+        ptn = re.compile(res)
+    except ValueError:
+        # re1.5 doesn't support all regex features
+        if res.startswith("(?ms)"):
+            res = res[5:]
+        if res.endswith("\\Z"):
+            res = res[:-2] + "$"
+        ptn = re.compile(res)
+
+    return ptn.match
+
+
+def filter(names, pat):
+    """Return the subset of the list NAMES that match PAT."""
+    result = []
+    pat = normcase(pat)
+    match = _compile_pattern(pat)
+    for name in names:
+        if match(normcase(name)):
+            result.append(name)
+    return result
+
+
+def fnmatchcase(name, pat):
+    """Test whether FILENAME matches PATTERN, including case.
+
+    This is a version of fnmatch() which doesn't case-normalize
+    its arguments.
+    """
+    match = _compile_pattern(pat)
+    return match(name) is not None
+
+
+def translate(pat):
+    """Translate a shell PATTERN to a regular expression.
+
+    There is no way to quote meta-characters.
+    """
+
+    i, n = 0, len(pat)
+    res = ""
+    while i < n:
+        c = pat[i]
+        i = i + 1
+        if c == "*":
+            res = res + ".*"
+        elif c == "?":
+            res = res + "."
+        elif c == "[":
+            j = i
+            if j < n and pat[j] == "!":
+                j = j + 1
+            if j < n and pat[j] == "]":
+                j = j + 1
+            while j < n and pat[j] != "]":
+                j = j + 1
+            if j >= n:
+                res = res + "\\["
+            else:
+                stuff = pat[i:j].replace("\\", "\\\\")
+                i = j + 1
+                if stuff[0] == "!":
+                    stuff = "^" + stuff[1:]
+                elif stuff[0] == "^":
+                    stuff = "\\" + stuff
+                res = "%s[%s]" % (res, stuff)
+        else:
+            try:
+                res = res + re.escape(c)
+            except AttributeError:
+                # Using ure rather than re-pcre
+                res = res + re_escape(c)
+    # Original patterns is undefined, see http://bugs.python.org/issue21464
+    return "(?ms)" + res + "\Z"
+
+
+def re_escape(pattern):
+    # Replacement minimal re.escape for ure compatibility
+    return re.sub(r"([\^\$\.\|\?\*\+\(\)\[\\])", r"\\\1", pattern)
diff --git a/python-stdlib/fnmatch/manifest.py b/python-stdlib/fnmatch/manifest.py
new file mode 100644
index 000000000..f4318b374
--- /dev/null
+++ b/python-stdlib/fnmatch/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.6.1")
+
+module("fnmatch.py")
diff --git a/python-stdlib/fnmatch/test_fnmatch.py b/python-stdlib/fnmatch/test_fnmatch.py
new file mode 100644
index 000000000..97ef8fff7
--- /dev/null
+++ b/python-stdlib/fnmatch/test_fnmatch.py
@@ -0,0 +1,80 @@
+"""Test cases for the fnmatch module."""
+
+import unittest
+
+from fnmatch import fnmatch, fnmatchcase, translate, filter
+
+
+class FnmatchTestCase(unittest.TestCase):
+    def check_match(self, filename, pattern, should_match=1, fn=fnmatch):
+        if should_match:
+            self.assertTrue(
+                fn(filename, pattern),
+                "expected %r to match pattern %r" % (filename, pattern),
+            )
+        else:
+            self.assertTrue(
+                not fn(filename, pattern),
+                "expected %r not to match pattern %r" % (filename, pattern),
+            )
+
+    def test_fnmatch(self):
+        check = self.check_match
+        check("abc", "abc")
+        check("abc", "?*?")
+        check("abc", "???*")
+        check("abc", "*???")
+        check("abc", "???")
+        check("abc", "*")
+        check("abc", "ab[cd]")
+        check("abc", "ab[!de]")
+        check("abc", "ab[de]", 0)
+        check("a", "??", 0)
+        check("a", "b", 0)
+
+        # these test that '\' is handled correctly in character sets;
+        # see SF bug #409651
+        check("\\", r"[\]")
+        check("a", r"[!\]")
+        check("\\", r"[!\]", 0)
+
+        # test that filenames with newlines in them are handled correctly.
+        # http://bugs.python.org/issue6665
+        check("foo\nbar", "foo*")
+        check("foo\nbar\n", "foo*")
+        check("\nfoo", "foo*", False)
+        check("\n", "*")
+
+    def _test_mix_bytes_str(self):
+        self.assertRaises(TypeError, fnmatch, "test", b"*")
+        self.assertRaises(TypeError, fnmatch, b"test", "*")
+        self.assertRaises(TypeError, fnmatchcase, "test", b"*")
+        self.assertRaises(TypeError, fnmatchcase, b"test", "*")
+
+    def test_fnmatchcase(self):
+        check = self.check_match
+        check("AbC", "abc", 0, fnmatchcase)
+        check("abc", "AbC", 0, fnmatchcase)
+
+    @unittest.skip("unsupported on MicroPython")
+    def test_bytes(self):
+        self.check_match(b"test", b"te*")
+        self.check_match(b"test\xff", b"te*\xff")
+        self.check_match(b"foo\nbar", b"foo*")
+
+
+class TranslateTestCase(unittest.TestCase):
+    def test_translate(self):
+        self.assertEqual(translate("*"), "(?ms).*\Z")
+        self.assertEqual(translate("?"), "(?ms).\Z")
+        self.assertEqual(translate("a?b*"), "(?ms)a.b.*\Z")
+        self.assertEqual(translate("[abc]"), "(?ms)[abc]\Z")
+        self.assertEqual(translate("[]]"), "(?ms)[]]\Z")
+        self.assertEqual(translate("[!x]"), "(?ms)[^x]\Z")
+        self.assertEqual(translate("[^x]"), "(?ms)[\\^x]\Z")
+        self.assertEqual(translate("[x"), "(?ms)\\[x\Z")
+
+
+class FilterTestCase(unittest.TestCase):
+    def test_filter(self):
+        self.assertEqual(filter(["a", "b"], "a"), ["a"])
diff --git a/python-stdlib/functools/functools.py b/python-stdlib/functools/functools.py
new file mode 100644
index 000000000..b3c368e8a
--- /dev/null
+++ b/python-stdlib/functools/functools.py
@@ -0,0 +1,28 @@
+def partial(func, *args, **kwargs):
+    def _partial(*more_args, **more_kwargs):
+        kw = kwargs.copy()
+        kw.update(more_kwargs)
+        return func(*(args + more_args), **kw)
+
+    return _partial
+
+
+def update_wrapper(wrapper, wrapped, assigned=None, updated=None):
+    # Dummy impl
+    return wrapper
+
+
+def wraps(wrapped, assigned=None, updated=None):
+    # Dummy impl
+    return lambda x: x
+
+
+def reduce(function, iterable, initializer=None):
+    it = iter(iterable)
+    if initializer is None:
+        value = next(it)
+    else:
+        value = initializer
+    for element in it:
+        value = function(value, element)
+    return value
diff --git a/python-stdlib/functools/manifest.py b/python-stdlib/functools/manifest.py
new file mode 100644
index 000000000..634413c1e
--- /dev/null
+++ b/python-stdlib/functools/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.0.7")
+
+module("functools.py")
diff --git a/functools/test_partial.py b/python-stdlib/functools/test_partial.py
similarity index 99%
rename from functools/test_partial.py
rename to python-stdlib/functools/test_partial.py
index 41305818c..be79af940 100644
--- a/functools/test_partial.py
+++ b/python-stdlib/functools/test_partial.py
@@ -1,5 +1,6 @@
 from functools import partial
 
+
 def foo(x, y, prompt="result:"):
     print(prompt, x + y)
 
diff --git a/python-stdlib/functools/test_reduce.py b/python-stdlib/functools/test_reduce.py
new file mode 100644
index 000000000..7a616d40f
--- /dev/null
+++ b/python-stdlib/functools/test_reduce.py
@@ -0,0 +1,7 @@
+from functools import reduce
+
+res = reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
+assert res == 1 + 2 + 3 + 4 + 5
+
+res = reduce(lambda x, y: x + y, [1, 2, 3, 4, 5], 10)
+assert res == 10 + 1 + 2 + 3 + 4 + 5
diff --git a/python-stdlib/gzip/gzip.py b/python-stdlib/gzip/gzip.py
new file mode 100644
index 000000000..12bfb1ff5
--- /dev/null
+++ b/python-stdlib/gzip/gzip.py
@@ -0,0 +1,29 @@
+# MicroPython gzip module
+# MIT license; Copyright (c) 2023 Jim Mussared
+
+_WBITS = const(15)
+
+import builtins, io, deflate
+
+
+def GzipFile(fileobj):
+    return deflate.DeflateIO(fileobj, deflate.GZIP, _WBITS)
+
+
+def open(filename, mode="rb"):
+    return deflate.DeflateIO(builtins.open(filename, mode), deflate.GZIP, _WBITS, True)
+
+
+if hasattr(deflate.DeflateIO, "write"):
+
+    def compress(data):
+        f = io.BytesIO()
+        with GzipFile(fileobj=f) as g:
+            g.write(data)
+        return f.getvalue()
+
+
+def decompress(data):
+    f = io.BytesIO(data)
+    with GzipFile(fileobj=f) as g:
+        return g.read()
diff --git a/python-stdlib/gzip/manifest.py b/python-stdlib/gzip/manifest.py
new file mode 100644
index 000000000..c422b2965
--- /dev/null
+++ b/python-stdlib/gzip/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="1.0.1")
+
+module("gzip.py")
diff --git a/python-stdlib/hashlib-core/hashlib/__init__.py b/python-stdlib/hashlib-core/hashlib/__init__.py
new file mode 100644
index 000000000..932b6f647
--- /dev/null
+++ b/python-stdlib/hashlib-core/hashlib/__init__.py
@@ -0,0 +1,29 @@
+# Use built-in algorithms preferentially (on many ports this is just sha256).
+try:
+    from uhashlib import *
+except ImportError:
+    pass
+
+
+# Add missing algorithms based on installed extensions.
+def _init():
+    for algo in ("sha224", "sha256", "sha384", "sha512"):
+        if algo not in globals():
+            try:
+                # from ._{algo} import {algo}
+                c = __import__("_" + algo, None, None, (), 1)
+                globals()[algo] = getattr(c, algo)
+            except ImportError:
+                pass
+
+
+_init()
+del _init
+
+
+def new(algo, data=b""):
+    try:
+        c = globals()[algo]
+        return c(data)
+    except KeyError:
+        raise ValueError(algo)
diff --git a/python-stdlib/hashlib-core/hashlib/_sha.py b/python-stdlib/hashlib-core/hashlib/_sha.py
new file mode 100644
index 000000000..4e7339c76
--- /dev/null
+++ b/python-stdlib/hashlib-core/hashlib/_sha.py
@@ -0,0 +1,42 @@
+# MIT license; Copyright (c) 2023 Jim Mussared
+# Originally ported from CPython by Paul Sokolovsky
+
+
+# Base class for SHA implementations, which must provide:
+#   .digestsize & .digest_size
+#   .block_size
+#   ._iv
+#   ._update
+#   ._final
+class sha:
+    def __init__(self, s=None):
+        self._digest = self._iv[:]
+        self._count_lo = 0
+        self._count_hi = 0
+        self._data = bytearray(self.block_size)
+        self._local = 0
+        self._digestsize = self.digest_size
+        if s:
+            self.update(s)
+
+    def update(self, s):
+        if isinstance(s, str):
+            s = s.encode("ascii")
+        else:
+            s = bytes(s)
+        self._update(s)
+
+    def digest(self):
+        return self.copy()._final()[: self._digestsize]
+
+    def hexdigest(self):
+        return "".join(["%.2x" % i for i in self.digest()])
+
+    def copy(self):
+        new = type(self)()
+        new._digest = self._digest[:]
+        new._count_lo = self._count_lo
+        new._count_hi = self._count_hi
+        new._data = self._data[:]
+        new._local = self._local
+        return new
diff --git a/python-stdlib/hashlib-core/manifest.py b/python-stdlib/hashlib-core/manifest.py
new file mode 100644
index 000000000..db8d42482
--- /dev/null
+++ b/python-stdlib/hashlib-core/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="1.0.0")
+
+package("hashlib")
diff --git a/python-stdlib/hashlib-sha224/hashlib/_sha224.py b/python-stdlib/hashlib-sha224/hashlib/_sha224.py
new file mode 100644
index 000000000..4f6dc7181
--- /dev/null
+++ b/python-stdlib/hashlib-sha224/hashlib/_sha224.py
@@ -0,0 +1,18 @@
+# MIT license; Copyright (c) 2023 Jim Mussared
+# Originally ported from CPython by Paul Sokolovsky
+
+from ._sha256 import sha256
+
+
+class sha224(sha256):
+    digest_size = digestsize = 28
+    _iv = [
+        0xC1059ED8,
+        0x367CD507,
+        0x3070DD17,
+        0xF70E5939,
+        0xFFC00B31,
+        0x68581511,
+        0x64F98FA7,
+        0xBEFA4FA4,
+    ]
diff --git a/python-stdlib/hashlib-sha224/manifest.py b/python-stdlib/hashlib-sha224/manifest.py
new file mode 100644
index 000000000..30a2c2531
--- /dev/null
+++ b/python-stdlib/hashlib-sha224/manifest.py
@@ -0,0 +1,4 @@
+metadata(version="1.0.0", description="Adds the SHA224 hash algorithm to hashlib.")
+
+require("hashlib-sha256")
+package("hashlib")
diff --git a/python-stdlib/hashlib-sha256/hashlib/_sha256.py b/python-stdlib/hashlib-sha256/hashlib/_sha256.py
new file mode 100644
index 000000000..43fc1522b
--- /dev/null
+++ b/python-stdlib/hashlib-sha256/hashlib/_sha256.py
@@ -0,0 +1,194 @@
+# MIT license; Copyright (c) 2023 Jim Mussared
+# Originally ported from CPython by Paul Sokolovsky
+
+from ._sha import sha
+
+_SHA_BLOCKSIZE = const(64)
+
+
+ROR = lambda x, y: (((x & 0xFFFFFFFF) >> (y & 31)) | (x << (32 - (y & 31)))) & 0xFFFFFFFF
+Ch = lambda x, y, z: (z ^ (x & (y ^ z)))
+Maj = lambda x, y, z: (((x | y) & z) | (x & y))
+S = lambda x, n: ROR(x, n)
+R = lambda x, n: (x & 0xFFFFFFFF) >> n
+Sigma0 = lambda x: (S(x, 2) ^ S(x, 13) ^ S(x, 22))
+Sigma1 = lambda x: (S(x, 6) ^ S(x, 11) ^ S(x, 25))
+Gamma0 = lambda x: (S(x, 7) ^ S(x, 18) ^ R(x, 3))
+Gamma1 = lambda x: (S(x, 17) ^ S(x, 19) ^ R(x, 10))
+
+
+class sha256(sha):
+    digest_size = digestsize = 32
+    block_size = _SHA_BLOCKSIZE
+    _iv = [
+        0x6A09E667,
+        0xBB67AE85,
+        0x3C6EF372,
+        0xA54FF53A,
+        0x510E527F,
+        0x9B05688C,
+        0x1F83D9AB,
+        0x5BE0CD19,
+    ]
+
+    def _transform(self):
+        W = []
+
+        d = self._data
+        for i in range(0, 16):
+            W.append((d[4 * i] << 24) + (d[4 * i + 1] << 16) + (d[4 * i + 2] << 8) + d[4 * i + 3])
+
+        for i in range(16, 64):
+            W.append((Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]) & 0xFFFFFFFF)
+
+        ss = self._digest[:]
+
+        def RND(a, b, c, d, e, f, g, h, i, ki):
+            t0 = h + Sigma1(e) + Ch(e, f, g) + ki + W[i]
+            t1 = Sigma0(a) + Maj(a, b, c)
+            d += t0
+            h = t0 + t1
+            return d & 0xFFFFFFFF, h & 0xFFFFFFFF
+
+        ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 0, 0x428A2F98)
+        ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 1, 0x71374491)
+        ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 2, 0xB5C0FBCF)
+        ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 3, 0xE9B5DBA5)
+        ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 4, 0x3956C25B)
+        ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 5, 0x59F111F1)
+        ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 6, 0x923F82A4)
+        ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 7, 0xAB1C5ED5)
+        ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 8, 0xD807AA98)
+        ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 9, 0x12835B01)
+        ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 10, 0x243185BE)
+        ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 11, 0x550C7DC3)
+        ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 12, 0x72BE5D74)
+        ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 13, 0x80DEB1FE)
+        ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 14, 0x9BDC06A7)
+        ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 15, 0xC19BF174)
+        ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 16, 0xE49B69C1)
+        ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 17, 0xEFBE4786)
+        ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 18, 0x0FC19DC6)
+        ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 19, 0x240CA1CC)
+        ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 20, 0x2DE92C6F)
+        ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 21, 0x4A7484AA)
+        ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 22, 0x5CB0A9DC)
+        ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 23, 0x76F988DA)
+        ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 24, 0x983E5152)
+        ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 25, 0xA831C66D)
+        ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 26, 0xB00327C8)
+        ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 27, 0xBF597FC7)
+        ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 28, 0xC6E00BF3)
+        ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 29, 0xD5A79147)
+        ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 30, 0x06CA6351)
+        ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 31, 0x14292967)
+        ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 32, 0x27B70A85)
+        ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 33, 0x2E1B2138)
+        ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 34, 0x4D2C6DFC)
+        ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 35, 0x53380D13)
+        ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 36, 0x650A7354)
+        ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 37, 0x766A0ABB)
+        ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 38, 0x81C2C92E)
+        ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 39, 0x92722C85)
+        ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 40, 0xA2BFE8A1)
+        ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 41, 0xA81A664B)
+        ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 42, 0xC24B8B70)
+        ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 43, 0xC76C51A3)
+        ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 44, 0xD192E819)
+        ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 45, 0xD6990624)
+        ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 46, 0xF40E3585)
+        ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 47, 0x106AA070)
+        ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 48, 0x19A4C116)
+        ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 49, 0x1E376C08)
+        ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 50, 0x2748774C)
+        ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 51, 0x34B0BCB5)
+        ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 52, 0x391C0CB3)
+        ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 53, 0x4ED8AA4A)
+        ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 54, 0x5B9CCA4F)
+        ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 55, 0x682E6FF3)
+        ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 56, 0x748F82EE)
+        ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 57, 0x78A5636F)
+        ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 58, 0x84C87814)
+        ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 59, 0x8CC70208)
+        ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 60, 0x90BEFFFA)
+        ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 61, 0xA4506CEB)
+        ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 62, 0xBEF9A3F7)
+        ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 63, 0xC67178F2)
+
+        for i in range(len(self._digest)):
+            self._digest[i] = (self._digest[i] + ss[i]) & 0xFFFFFFFF
+
+    def _update(self, buffer):
+        if isinstance(buffer, str):
+            raise TypeError("Unicode strings must be encoded before hashing")
+        count = len(buffer)
+        buffer_idx = 0
+        clo = (self._count_lo + (count << 3)) & 0xFFFFFFFF
+        if clo < self._count_lo:
+            self._count_hi += 1
+        self._count_lo = clo
+
+        self._count_hi += count >> 29
+
+        if self._local:
+            i = _SHA_BLOCKSIZE - self._local
+            if i > count:
+                i = count
+
+            # copy buffer
+            for x in enumerate(buffer[buffer_idx : buffer_idx + i]):
+                self._data[self._local + x[0]] = x[1]
+
+            count -= i
+            buffer_idx += i
+
+            self._local += i
+            if self._local == _SHA_BLOCKSIZE:
+                self._transform()
+                self._local = 0
+            else:
+                return
+
+        while count >= _SHA_BLOCKSIZE:
+            # copy buffer
+            self._data = bytearray(buffer[buffer_idx : buffer_idx + _SHA_BLOCKSIZE])
+            count -= _SHA_BLOCKSIZE
+            buffer_idx += _SHA_BLOCKSIZE
+            self._transform()
+
+        # copy buffer
+        pos = self._local
+        self._data[pos : pos + count] = buffer[buffer_idx : buffer_idx + count]
+        self._local = count
+
+    def _final(self):
+        lo_bit_count = self._count_lo
+        hi_bit_count = self._count_hi
+        count = (lo_bit_count >> 3) & 0x3F
+        self._data[count] = 0x80
+        count += 1
+        if count > _SHA_BLOCKSIZE - 8:
+            # zero the bytes in data after the count
+            self._data = self._data[:count] + bytes(_SHA_BLOCKSIZE - count)
+            self._transform()
+            # zero bytes in data
+            self._data = bytearray(_SHA_BLOCKSIZE)
+        else:
+            self._data = self._data[:count] + bytes(_SHA_BLOCKSIZE - count)
+
+        self._data[56] = (hi_bit_count >> 24) & 0xFF
+        self._data[57] = (hi_bit_count >> 16) & 0xFF
+        self._data[58] = (hi_bit_count >> 8) & 0xFF
+        self._data[59] = (hi_bit_count >> 0) & 0xFF
+        self._data[60] = (lo_bit_count >> 24) & 0xFF
+        self._data[61] = (lo_bit_count >> 16) & 0xFF
+        self._data[62] = (lo_bit_count >> 8) & 0xFF
+        self._data[63] = (lo_bit_count >> 0) & 0xFF
+
+        self._transform()
+
+        dig = bytearray()
+        for i in self._digest:
+            for j in range(4):
+                dig.append((i >> ((3 - j) * 8)) & 0xFF)
+        return dig
diff --git a/python-stdlib/hashlib-sha256/manifest.py b/python-stdlib/hashlib-sha256/manifest.py
new file mode 100644
index 000000000..42a859e1d
--- /dev/null
+++ b/python-stdlib/hashlib-sha256/manifest.py
@@ -0,0 +1,4 @@
+metadata(version="1.0.0", description="Adds the SHA256 hash algorithm to hashlib.")
+
+require("hashlib-core")
+package("hashlib")
diff --git a/python-stdlib/hashlib-sha384/hashlib/_sha384.py b/python-stdlib/hashlib-sha384/hashlib/_sha384.py
new file mode 100644
index 000000000..fe15a10af
--- /dev/null
+++ b/python-stdlib/hashlib-sha384/hashlib/_sha384.py
@@ -0,0 +1,18 @@
+# MIT license; Copyright (c) 2023 Jim Mussared
+# Originally ported from CPython by Paul Sokolovsky
+
+from ._sha512 import sha512
+
+
+class sha384(sha512):
+    digest_size = digestsize = 48
+    _iv = [
+        0xCBBB9D5DC1059ED8,
+        0x629A292A367CD507,
+        0x9159015A3070DD17,
+        0x152FECD8F70E5939,
+        0x67332667FFC00B31,
+        0x8EB44A8768581511,
+        0xDB0C2E0D64F98FA7,
+        0x47B5481DBEFA4FA4,
+    ]
diff --git a/python-stdlib/hashlib-sha384/manifest.py b/python-stdlib/hashlib-sha384/manifest.py
new file mode 100644
index 000000000..6791eb56c
--- /dev/null
+++ b/python-stdlib/hashlib-sha384/manifest.py
@@ -0,0 +1,4 @@
+metadata(version="1.0.0", description="Adds the SHA384 hash algorithm to hashlib.")
+
+require("hashlib-sha512")
+package("hashlib")
diff --git a/python-stdlib/hashlib-sha512/hashlib/_sha512.py b/python-stdlib/hashlib-sha512/hashlib/_sha512.py
new file mode 100644
index 000000000..44e8656a6
--- /dev/null
+++ b/python-stdlib/hashlib-sha512/hashlib/_sha512.py
@@ -0,0 +1,393 @@
+# MIT license; Copyright (c) 2023 Jim Mussared
+# Originally ported from CPython by Paul Sokolovsky
+
+from ._sha import sha
+
+_SHA_BLOCKSIZE = const(128)
+
+
+ROR64 = (
+    lambda x, y: (((x & 0xFFFFFFFFFFFFFFFF) >> (y & 63)) | (x << (64 - (y & 63))))
+    & 0xFFFFFFFFFFFFFFFF
+)
+Ch = lambda x, y, z: (z ^ (x & (y ^ z)))
+Maj = lambda x, y, z: (((x | y) & z) | (x & y))
+S = lambda x, n: ROR64(x, n)
+R = lambda x, n: (x & 0xFFFFFFFFFFFFFFFF) >> n
+Sigma0 = lambda x: (S(x, 28) ^ S(x, 34) ^ S(x, 39))
+Sigma1 = lambda x: (S(x, 14) ^ S(x, 18) ^ S(x, 41))
+Gamma0 = lambda x: (S(x, 1) ^ S(x, 8) ^ R(x, 7))
+Gamma1 = lambda x: (S(x, 19) ^ S(x, 61) ^ R(x, 6))
+
+
+class sha512(sha):
+    digest_size = digestsize = 64
+    block_size = _SHA_BLOCKSIZE
+    _iv = [
+        0x6A09E667F3BCC908,
+        0xBB67AE8584CAA73B,
+        0x3C6EF372FE94F82B,
+        0xA54FF53A5F1D36F1,
+        0x510E527FADE682D1,
+        0x9B05688C2B3E6C1F,
+        0x1F83D9ABFB41BD6B,
+        0x5BE0CD19137E2179,
+    ]
+
+    def _transform(self):
+        W = []
+
+        d = self._data
+        for i in range(0, 16):
+            W.append(
+                (d[8 * i] << 56)
+                + (d[8 * i + 1] << 48)
+                + (d[8 * i + 2] << 40)
+                + (d[8 * i + 3] << 32)
+                + (d[8 * i + 4] << 24)
+                + (d[8 * i + 5] << 16)
+                + (d[8 * i + 6] << 8)
+                + d[8 * i + 7]
+            )
+
+        for i in range(16, 80):
+            W.append(
+                (Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]) & 0xFFFFFFFFFFFFFFFF
+            )
+
+        ss = self._digest[:]
+
+        def RND(a, b, c, d, e, f, g, h, i, ki):
+            t0 = (h + Sigma1(e) + Ch(e, f, g) + ki + W[i]) & 0xFFFFFFFFFFFFFFFF
+            t1 = (Sigma0(a) + Maj(a, b, c)) & 0xFFFFFFFFFFFFFFFF
+            d = (d + t0) & 0xFFFFFFFFFFFFFFFF
+            h = (t0 + t1) & 0xFFFFFFFFFFFFFFFF
+            return d & 0xFFFFFFFFFFFFFFFF, h & 0xFFFFFFFFFFFFFFFF
+
+        ss[3], ss[7] = RND(
+            ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 0, 0x428A2F98D728AE22
+        )
+        ss[2], ss[6] = RND(
+            ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 1, 0x7137449123EF65CD
+        )
+        ss[1], ss[5] = RND(
+            ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 2, 0xB5C0FBCFEC4D3B2F
+        )
+        ss[0], ss[4] = RND(
+            ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 3, 0xE9B5DBA58189DBBC
+        )
+        ss[7], ss[3] = RND(
+            ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 4, 0x3956C25BF348B538
+        )
+        ss[6], ss[2] = RND(
+            ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 5, 0x59F111F1B605D019
+        )
+        ss[5], ss[1] = RND(
+            ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 6, 0x923F82A4AF194F9B
+        )
+        ss[4], ss[0] = RND(
+            ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 7, 0xAB1C5ED5DA6D8118
+        )
+        ss[3], ss[7] = RND(
+            ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 8, 0xD807AA98A3030242
+        )
+        ss[2], ss[6] = RND(
+            ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 9, 0x12835B0145706FBE
+        )
+        ss[1], ss[5] = RND(
+            ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 10, 0x243185BE4EE4B28C
+        )
+        ss[0], ss[4] = RND(
+            ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 11, 0x550C7DC3D5FFB4E2
+        )
+        ss[7], ss[3] = RND(
+            ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 12, 0x72BE5D74F27B896F
+        )
+        ss[6], ss[2] = RND(
+            ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 13, 0x80DEB1FE3B1696B1
+        )
+        ss[5], ss[1] = RND(
+            ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 14, 0x9BDC06A725C71235
+        )
+        ss[4], ss[0] = RND(
+            ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 15, 0xC19BF174CF692694
+        )
+        ss[3], ss[7] = RND(
+            ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 16, 0xE49B69C19EF14AD2
+        )
+        ss[2], ss[6] = RND(
+            ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 17, 0xEFBE4786384F25E3
+        )
+        ss[1], ss[5] = RND(
+            ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 18, 0x0FC19DC68B8CD5B5
+        )
+        ss[0], ss[4] = RND(
+            ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 19, 0x240CA1CC77AC9C65
+        )
+        ss[7], ss[3] = RND(
+            ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 20, 0x2DE92C6F592B0275
+        )
+        ss[6], ss[2] = RND(
+            ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 21, 0x4A7484AA6EA6E483
+        )
+        ss[5], ss[1] = RND(
+            ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 22, 0x5CB0A9DCBD41FBD4
+        )
+        ss[4], ss[0] = RND(
+            ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 23, 0x76F988DA831153B5
+        )
+        ss[3], ss[7] = RND(
+            ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 24, 0x983E5152EE66DFAB
+        )
+        ss[2], ss[6] = RND(
+            ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 25, 0xA831C66D2DB43210
+        )
+        ss[1], ss[5] = RND(
+            ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 26, 0xB00327C898FB213F
+        )
+        ss[0], ss[4] = RND(
+            ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 27, 0xBF597FC7BEEF0EE4
+        )
+        ss[7], ss[3] = RND(
+            ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 28, 0xC6E00BF33DA88FC2
+        )
+        ss[6], ss[2] = RND(
+            ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 29, 0xD5A79147930AA725
+        )
+        ss[5], ss[1] = RND(
+            ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 30, 0x06CA6351E003826F
+        )
+        ss[4], ss[0] = RND(
+            ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 31, 0x142929670A0E6E70
+        )
+        ss[3], ss[7] = RND(
+            ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 32, 0x27B70A8546D22FFC
+        )
+        ss[2], ss[6] = RND(
+            ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 33, 0x2E1B21385C26C926
+        )
+        ss[1], ss[5] = RND(
+            ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 34, 0x4D2C6DFC5AC42AED
+        )
+        ss[0], ss[4] = RND(
+            ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 35, 0x53380D139D95B3DF
+        )
+        ss[7], ss[3] = RND(
+            ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 36, 0x650A73548BAF63DE
+        )
+        ss[6], ss[2] = RND(
+            ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 37, 0x766A0ABB3C77B2A8
+        )
+        ss[5], ss[1] = RND(
+            ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 38, 0x81C2C92E47EDAEE6
+        )
+        ss[4], ss[0] = RND(
+            ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 39, 0x92722C851482353B
+        )
+        ss[3], ss[7] = RND(
+            ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 40, 0xA2BFE8A14CF10364
+        )
+        ss[2], ss[6] = RND(
+            ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 41, 0xA81A664BBC423001
+        )
+        ss[1], ss[5] = RND(
+            ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 42, 0xC24B8B70D0F89791
+        )
+        ss[0], ss[4] = RND(
+            ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 43, 0xC76C51A30654BE30
+        )
+        ss[7], ss[3] = RND(
+            ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 44, 0xD192E819D6EF5218
+        )
+        ss[6], ss[2] = RND(
+            ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 45, 0xD69906245565A910
+        )
+        ss[5], ss[1] = RND(
+            ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 46, 0xF40E35855771202A
+        )
+        ss[4], ss[0] = RND(
+            ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 47, 0x106AA07032BBD1B8
+        )
+        ss[3], ss[7] = RND(
+            ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 48, 0x19A4C116B8D2D0C8
+        )
+        ss[2], ss[6] = RND(
+            ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 49, 0x1E376C085141AB53
+        )
+        ss[1], ss[5] = RND(
+            ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 50, 0x2748774CDF8EEB99
+        )
+        ss[0], ss[4] = RND(
+            ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 51, 0x34B0BCB5E19B48A8
+        )
+        ss[7], ss[3] = RND(
+            ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 52, 0x391C0CB3C5C95A63
+        )
+        ss[6], ss[2] = RND(
+            ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 53, 0x4ED8AA4AE3418ACB
+        )
+        ss[5], ss[1] = RND(
+            ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 54, 0x5B9CCA4F7763E373
+        )
+        ss[4], ss[0] = RND(
+            ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 55, 0x682E6FF3D6B2B8A3
+        )
+        ss[3], ss[7] = RND(
+            ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 56, 0x748F82EE5DEFB2FC
+        )
+        ss[2], ss[6] = RND(
+            ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 57, 0x78A5636F43172F60
+        )
+        ss[1], ss[5] = RND(
+            ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 58, 0x84C87814A1F0AB72
+        )
+        ss[0], ss[4] = RND(
+            ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 59, 0x8CC702081A6439EC
+        )
+        ss[7], ss[3] = RND(
+            ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 60, 0x90BEFFFA23631E28
+        )
+        ss[6], ss[2] = RND(
+            ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 61, 0xA4506CEBDE82BDE9
+        )
+        ss[5], ss[1] = RND(
+            ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 62, 0xBEF9A3F7B2C67915
+        )
+        ss[4], ss[0] = RND(
+            ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 63, 0xC67178F2E372532B
+        )
+        ss[3], ss[7] = RND(
+            ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 64, 0xCA273ECEEA26619C
+        )
+        ss[2], ss[6] = RND(
+            ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 65, 0xD186B8C721C0C207
+        )
+        ss[1], ss[5] = RND(
+            ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 66, 0xEADA7DD6CDE0EB1E
+        )
+        ss[0], ss[4] = RND(
+            ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 67, 0xF57D4F7FEE6ED178
+        )
+        ss[7], ss[3] = RND(
+            ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 68, 0x06F067AA72176FBA
+        )
+        ss[6], ss[2] = RND(
+            ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 69, 0x0A637DC5A2C898A6
+        )
+        ss[5], ss[1] = RND(
+            ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 70, 0x113F9804BEF90DAE
+        )
+        ss[4], ss[0] = RND(
+            ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 71, 0x1B710B35131C471B
+        )
+        ss[3], ss[7] = RND(
+            ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 72, 0x28DB77F523047D84
+        )
+        ss[2], ss[6] = RND(
+            ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 73, 0x32CAAB7B40C72493
+        )
+        ss[1], ss[5] = RND(
+            ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 74, 0x3C9EBE0A15C9BEBC
+        )
+        ss[0], ss[4] = RND(
+            ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 75, 0x431D67C49C100D4C
+        )
+        ss[7], ss[3] = RND(
+            ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 76, 0x4CC5D4BECB3E42B6
+        )
+        ss[6], ss[2] = RND(
+            ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 77, 0x597F299CFC657E2A
+        )
+        ss[5], ss[1] = RND(
+            ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 78, 0x5FCB6FAB3AD6FAEC
+        )
+        ss[4], ss[0] = RND(
+            ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 79, 0x6C44198C4A475817
+        )
+
+        for i in range(len(self._digest)):
+            self._digest[i] = (self._digest[i] + ss[i]) & 0xFFFFFFFFFFFFFFFF
+
+    def _update(self, buffer):
+        if isinstance(buffer, str):
+            raise TypeError("Unicode strings must be encoded before hashing")
+        count = len(buffer)
+        buffer_idx = 0
+        clo = (self._count_lo + (count << 3)) & 0xFFFFFFFF
+        if clo < self._count_lo:
+            self._count_hi += 1
+        self._count_lo = clo
+
+        self._count_hi += count >> 29
+
+        if self._local:
+            i = _SHA_BLOCKSIZE - self._local
+            if i > count:
+                i = count
+
+            # copy buffer
+            for x in enumerate(buffer[buffer_idx : buffer_idx + i]):
+                self._data[self._local + x[0]] = x[1]
+
+            count -= i
+            buffer_idx += i
+
+            self._local += i
+            if self._local == _SHA_BLOCKSIZE:
+                self._transform()
+                self._local = 0
+            else:
+                return
+
+        while count >= _SHA_BLOCKSIZE:
+            # copy buffer
+            self._data = bytearray(buffer[buffer_idx : buffer_idx + _SHA_BLOCKSIZE])
+            count -= _SHA_BLOCKSIZE
+            buffer_idx += _SHA_BLOCKSIZE
+            self._transform()
+
+        # copy buffer
+        pos = self._local
+        self._data[pos : pos + count] = buffer[buffer_idx : buffer_idx + count]
+        self._local = count
+
+    def _final(self):
+        lo_bit_count = self._count_lo
+        hi_bit_count = self._count_hi
+        count = (lo_bit_count >> 3) & 0x7F
+        self._data[count] = 0x80
+        count += 1
+        if count > _SHA_BLOCKSIZE - 16:
+            # zero the bytes in data after the count
+            self._data = self._data[:count] + bytes(_SHA_BLOCKSIZE - count)
+            self._transform()
+            # zero bytes in data
+            self._data = bytearray(_SHA_BLOCKSIZE)
+        else:
+            self._data = self._data[:count] + bytes(_SHA_BLOCKSIZE - count)
+
+        self._data[112] = 0
+        self._data[113] = 0
+        self._data[114] = 0
+        self._data[115] = 0
+        self._data[116] = 0
+        self._data[117] = 0
+        self._data[118] = 0
+        self._data[119] = 0
+
+        self._data[120] = (hi_bit_count >> 24) & 0xFF
+        self._data[121] = (hi_bit_count >> 16) & 0xFF
+        self._data[122] = (hi_bit_count >> 8) & 0xFF
+        self._data[123] = (hi_bit_count >> 0) & 0xFF
+        self._data[124] = (lo_bit_count >> 24) & 0xFF
+        self._data[125] = (lo_bit_count >> 16) & 0xFF
+        self._data[126] = (lo_bit_count >> 8) & 0xFF
+        self._data[127] = (lo_bit_count >> 0) & 0xFF
+
+        self._transform()
+
+        dig = bytearray()
+        for i in self._digest:
+            for j in range(8):
+                dig.append((i >> ((7 - j) * 8)) & 0xFF)
+        return dig
diff --git a/python-stdlib/hashlib-sha512/manifest.py b/python-stdlib/hashlib-sha512/manifest.py
new file mode 100644
index 000000000..1d84f025a
--- /dev/null
+++ b/python-stdlib/hashlib-sha512/manifest.py
@@ -0,0 +1,4 @@
+metadata(version="1.0.0", description="Adds the SHA512 hash algorithm to hashlib.")
+
+require("hashlib-core")
+package("hashlib")
diff --git a/python-stdlib/hashlib/manifest.py b/python-stdlib/hashlib/manifest.py
new file mode 100644
index 000000000..01f745e14
--- /dev/null
+++ b/python-stdlib/hashlib/manifest.py
@@ -0,0 +1,8 @@
+metadata(version="2.5.0")
+
+# This is a collection package that gets all hash functions. To save code and
+# memory size, prefer to install just the algorithm you need.
+require("hashlib-sha224")
+require("hashlib-sha256")
+require("hashlib-sha384")
+require("hashlib-sha512")
diff --git a/python-stdlib/hashlib/tests/test_new.py b/python-stdlib/hashlib/tests/test_new.py
new file mode 100644
index 000000000..f844a1ccd
--- /dev/null
+++ b/python-stdlib/hashlib/tests/test_new.py
@@ -0,0 +1,32 @@
+import unittest
+import hashlib
+
+
+class TestNew(unittest.TestCase):
+    def test_sha224(self):
+        self.assertEqual(
+            hashlib.new("sha224", b"1234").digest(),
+            b"\x99\xfb/H\xc6\xafGa\xf9\x04\xfc\x85\xf9^\xb5a\x90\xe5\xd4\x0b\x1fD\xec:\x9c\x1f\xa3\x19",
+        )
+
+    def test_sha256(self):
+        self.assertEqual(
+            hashlib.new("sha256", b"1234").digest(),
+            b"\x03\xacgB\x16\xf3\xe1\\v\x1e\xe1\xa5\xe2U\xf0g\x956#\xc8\xb3\x88\xb4E\x9e\x13\xf9x\xd7\xc8F\xf4",
+        )
+
+    def test_sha384(self):
+        self.assertEqual(
+            hashlib.new("sha384", b"1234").digest(),
+            b"PO\x00\x8c\x8f\xcf\x8b.\xd5\xdf\xcd\xe7R\xfcTd\xab\x8b\xa0d!]\x9c[_\xc4\x86\xaf=\x9a\xb8\xc8\x1b\x14xQ\x80\xd2\xad|\xee\x1a\xb7\x92\xadDy\x8c",
+        )
+
+    def test_sha512(self):
+        self.assertEqual(
+            hashlib.new("sha512", b"1234").digest(),
+            b"\xd4\x04U\x9f`.\xabo\xd6\x02\xacv\x80\xda\xcb\xfa\xad\xd1603^\x95\x1f\tz\xf3\x90\x0e\x9d\xe1v\xb6\xdb(Q/.\x00\x0b\x9d\x04\xfb\xa5\x13>\x8b\x1cn\x8d\xf5\x9d\xb3\xa8\xab\x9d`\xbeK\x97\xcc\x9e\x81\xdb",
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python-stdlib/hashlib/tests/test_sha256.py b/python-stdlib/hashlib/tests/test_sha256.py
new file mode 100644
index 000000000..a311a8cc9
--- /dev/null
+++ b/python-stdlib/hashlib/tests/test_sha256.py
@@ -0,0 +1,87 @@
+# Prevent importing any built-in hashes, so this test tests only the pure Python hashes.
+import sys
+sys.modules['uhashlib'] = sys
+
+import unittest
+from hashlib import sha256
+
+
+class TestSha256(unittest.TestCase):
+    a_str = b"just a test string"
+    b_str = b"some other string for testing"
+    c_str = b"nothing to see here"
+
+    def test_empty(self):
+        self.assertEqual(
+            b"\xe3\xb0\xc4B\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99o\xb9$'\xaeA\xe4d\x9b\x93L\xa4\x95\x99\x1bxR\xb8U",
+            sha256().digest(),
+        )
+
+    def test_empty_hex(self):
+        self.assertEqual(
+            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            sha256().hexdigest(),
+        )
+
+    def test_str(self):
+        self.assertEqual(
+            b"\xd7\xb5S\xc6\xf0\x9a\xc8]\x14$\x15\xf8W\xc51\x0f;\xbb\xe7\xcd\xd7\x87\xcc\xe4\xb9\x85\xac\xed\xd5\x85&o",
+            sha256(self.a_str).digest(),
+        )
+        self.assertEqual(
+            b'|\x80Q\xb2\xa0u\xf0g\xe3\xc45\xce1p\xc6I\xb6r\x19J&\x8b\xdc\xa5"\x00?A\x90\xba\xbd,',
+            sha256(self.b_str).digest(),
+        )
+
+    def test_str_hex(self):
+        self.assertEqual(
+            "d7b553c6f09ac85d142415f857c5310f3bbbe7cdd787cce4b985acedd585266f",
+            sha256(self.a_str).hexdigest(),
+        )
+        self.assertEqual(
+            "7c8051b2a075f067e3c435ce3170c649b672194a268bdca522003f4190babd2c",
+            sha256(self.b_str).hexdigest(),
+        )
+
+    def test_long_str(self):
+        self.assertEqual(
+            "f1f1af5d66ba1789f8214354c0ed04856bbe43c01aa392c584ef1ec3dbf45482",
+            sha256(self.a_str * 123).hexdigest(),
+        )
+
+    def test_update(self):
+        s = sha256(self.a_str)
+        s.update(self.b_str)
+        self.assertEqual(
+            "fc7f204eb969ca3f10488731fa63910486adda7c2ae2ee2142e85414454c6d42", s.hexdigest()
+        )
+
+    def test_repeat_final(self):
+        s = sha256(self.a_str)
+        s.update(self.b_str)
+        self.assertEqual(
+            "fc7f204eb969ca3f10488731fa63910486adda7c2ae2ee2142e85414454c6d42", s.hexdigest()
+        )
+        self.assertEqual(
+            "fc7f204eb969ca3f10488731fa63910486adda7c2ae2ee2142e85414454c6d42", s.hexdigest()
+        )
+        s.update(self.c_str)
+        self.assertEqual(
+            "b707db9ae915b0f6f9a67ded8c9932999ee7e9dfb33513b084ea9384f5ffb082", s.hexdigest()
+        )
+
+    def test_copy(self):
+        s = sha256(self.a_str)
+        s2 = s.copy()
+        s.update(self.b_str)
+        s2.update(self.c_str)
+        self.assertEqual(
+            "fc7f204eb969ca3f10488731fa63910486adda7c2ae2ee2142e85414454c6d42", s.hexdigest()
+        )
+        self.assertEqual(
+            "6a340b2bd2b63f4a0f9bb7566c26831354ee6ed17d1187d3a53627181fcb2907", s2.hexdigest()
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python-stdlib/hashlib/tests/test_sha512.py b/python-stdlib/hashlib/tests/test_sha512.py
new file mode 100644
index 000000000..9f80606f3
--- /dev/null
+++ b/python-stdlib/hashlib/tests/test_sha512.py
@@ -0,0 +1,89 @@
+import unittest
+from hashlib import sha512
+
+
+class Testsha512(unittest.TestCase):
+    a_str = b"just a test string"
+    b_str = b"some other string for testing"
+    c_str = b"nothing to see here"
+
+    def test_empty(self):
+        self.assertEqual(
+            b"\xcf\x83\xe15~\xef\xb8\xbd\xf1T(P\xd6m\x80\x07\xd6 \xe4\x05\x0bW\x15\xdc\x83\xf4\xa9!\xd3l\xe9\xceG\xd0\xd1<]\x85\xf2\xb0\xff\x83\x18\xd2\x87~\xec/c\xb91\xbdGAz\x81\xa582z\xf9'\xda>",
+            sha512().digest(),
+        )
+
+    def test_empty_hex(self):
+        self.assertEqual(
+            "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
+            sha512().hexdigest(),
+        )
+
+    def test_str(self):
+        self.assertEqual(
+            b"h\xbeLfd\xaf\x86}\xd1\xd0\x1c\x8dw\xe9c\xd8}w\xb7\x02@\x0c\x8f\xab\xae5ZA\xb8\x92zZU3\xa7\xf1\xc2\x85\t\xbb\xd6\\_:\xc7\x16\xf3;\xe2q\xfb\xda\x0c\xa0\x18\xb7\x1a\x84p\x8c\x9f\xae\x8aS",
+            sha512(self.a_str).digest(),
+        )
+        self.assertEqual(
+            b"Tt\xd1\xf8\x1fh\x14\xba\x85\x1a\x84\x15\x9b(\x812\x8er\x8d\xdeN\xc0\xe2\xff\xbb\xcc$i\x18gh\x18\xc4\xcb?\xc0\xa0\nTl\x0f\x01J\x07eP\x19\x98\xd9\xebZ\xd2?\x1cj\xa8Q)!\x18\xab!!~",
+            sha512(self.b_str).digest(),
+        )
+
+    def test_str_hex(self):
+        self.assertEqual(
+            "68be4c6664af867dd1d01c8d77e963d87d77b702400c8fabae355a41b8927a5a5533a7f1c28509bbd65c5f3ac716f33be271fbda0ca018b71a84708c9fae8a53",
+            sha512(self.a_str).hexdigest(),
+        )
+        self.assertEqual(
+            "5474d1f81f6814ba851a84159b2881328e728dde4ec0e2ffbbcc246918676818c4cb3fc0a00a546c0f014a0765501998d9eb5ad23f1c6aa851292118ab21217e",
+            sha512(self.b_str).hexdigest(),
+        )
+
+    def test_long_str(self):
+        self.assertEqual(
+            "8ee045cd8faf900bb23d13754d65723404a224030af827897cde92a40f7a1202405bc3efe5466c7e4833e7a9a5b9f9278ebe4c968e7fa662d8addc17ba95cc73",
+            sha512(self.a_str * 123).hexdigest(),
+        )
+
+    def test_update(self):
+        s = sha512(self.a_str)
+        s.update(self.b_str)
+        self.assertEqual(
+            "3fa253e7b093d5bc7b31f613f03833a4d39341cf73642349a46f26b39b5d95c97bb4e16fc588bda81d5c7a2db62cfca5c4c71a142cf02fd78409bffe5e4f408c",
+            s.hexdigest(),
+        )
+
+    def test_repeat_final(self):
+        s = sha512(self.a_str)
+        s.update(self.b_str)
+        self.assertEqual(
+            "3fa253e7b093d5bc7b31f613f03833a4d39341cf73642349a46f26b39b5d95c97bb4e16fc588bda81d5c7a2db62cfca5c4c71a142cf02fd78409bffe5e4f408c",
+            s.hexdigest(),
+        )
+        self.assertEqual(
+            "3fa253e7b093d5bc7b31f613f03833a4d39341cf73642349a46f26b39b5d95c97bb4e16fc588bda81d5c7a2db62cfca5c4c71a142cf02fd78409bffe5e4f408c",
+            s.hexdigest(),
+        )
+        s.update(self.c_str)
+        self.assertEqual(
+            "4b0827d5a28eeb2ebbeec270d7c775e78d5a76251753b8242327ffa2b1e5662a655be44bc09e41fcc0805bccd79cee13f4c41c40acff6fc1cf69b311d9b08f55",
+            s.hexdigest(),
+        )
+
+    def test_copy(self):
+        s = sha512(self.a_str)
+        s2 = s.copy()
+        s.update(self.b_str)
+        s2.update(self.c_str)
+        self.assertEqual(
+            "3fa253e7b093d5bc7b31f613f03833a4d39341cf73642349a46f26b39b5d95c97bb4e16fc588bda81d5c7a2db62cfca5c4c71a142cf02fd78409bffe5e4f408c",
+            s.hexdigest(),
+        )
+        self.assertEqual(
+            "2e4d68ec2d2836f24718b24442db027141fd2f7e06fb11c1460b013017feb0e74dea9d9415abe51b729ad86792bd5cd2cec9567d58a47a03785028376e7a5cc1",
+            s2.hexdigest(),
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/heapq/heapq.py b/python-stdlib/heapq/heapq.py
similarity index 91%
rename from heapq/heapq.py
rename to python-stdlib/heapq/heapq.py
index 8b278fb8c..b11853b8d 100644
--- a/heapq/heapq.py
+++ b/python-stdlib/heapq/heapq.py
@@ -124,19 +124,29 @@
 From all times, sorting has always been a Great Art! :-)
 """
 
-__all__ = ['heappush', 'heappop', 'heapify', 'heapreplace', 'merge',
-           'nlargest', 'nsmallest', 'heappushpop']
+__all__ = [
+    "heappush",
+    "heappop",
+    "heapify",
+    "heapreplace",
+    "merge",
+    "nlargest",
+    "nsmallest",
+    "heappushpop",
+]
+
+# from itertools import count, tee, chain
 
-#from itertools import count, tee, chain
 
 def heappush(heap, item):
     """Push item onto heap, maintaining the heap invariant."""
     heap.append(item)
-    _siftdown(heap, 0, len(heap)-1)
+    _siftdown(heap, 0, len(heap) - 1)
+
 
 def heappop(heap):
     """Pop the smallest item off the heap, maintaining the heap invariant."""
-    lastelt = heap.pop()    # raises appropriate IndexError if heap is empty
+    lastelt = heap.pop()  # raises appropriate IndexError if heap is empty
     if heap:
         returnitem = heap[0]
         heap[0] = lastelt
@@ -145,6 +155,7 @@ def heappop(heap):
         returnitem = lastelt
     return returnitem
 
+
 def heapreplace(heap, item):
     """Pop and return the current smallest value, and add the new item.
 
@@ -156,11 +167,12 @@ def heapreplace(heap, item):
         if item > heap[0]:
             item = heapreplace(heap, item)
     """
-    returnitem = heap[0]    # raises appropriate IndexError if heap is empty
+    returnitem = heap[0]  # raises appropriate IndexError if heap is empty
     heap[0] = item
     _siftup(heap, 0)
     return returnitem
 
+
 def heappushpop(heap, item):
     """Fast version of a heappush followed by a heappop."""
     if heap and heap[0] < item:
@@ -168,6 +180,7 @@ def heappushpop(heap, item):
         _siftup(heap, 0)
     return item
 
+
 def heapify(x):
     """Transform list into a heap, in-place, in O(len(x)) time."""
     n = len(x)
@@ -176,9 +189,10 @@ def heapify(x):
     # or i < (n-1)/2.  If n is even = 2*j, this is (2*j-1)/2 = j-1/2 so
     # j-1 is the largest, which is n//2 - 1.  If n is odd = 2*j+1, this is
     # (2*j+1-1)/2 = j so j-1 is the largest, and that's again n//2-1.
-    for i in reversed(range(n//2)):
+    for i in reversed(range(n // 2)):
         _siftup(x, i)
 
+
 def _heappushpop_max(heap, item):
     """Maxheap version of a heappush followed by a heappop."""
     if heap and item < heap[0]:
@@ -186,18 +200,21 @@ def _heappushpop_max(heap, item):
         _siftup_max(heap, 0)
     return item
 
+
 def _heapify_max(x):
     """Transform list into a maxheap, in-place, in O(len(x)) time."""
     n = len(x)
-    for i in reversed(range(n//2)):
+    for i in reversed(range(n // 2)):
         _siftup_max(x, i)
 
+
 def nlargest(n, iterable):
     """Find the n largest elements in a dataset.
 
     Equivalent to:  sorted(iterable, reverse=True)[:n]
     """
     from itertools import islice, count, tee, chain
+
     if n < 0:
         return []
     it = iter(iterable)
@@ -211,12 +228,14 @@ def nlargest(n, iterable):
     result.sort(reverse=True)
     return result
 
+
 def nsmallest(n, iterable):
     """Find the n smallest elements in a dataset.
 
     Equivalent to:  sorted(iterable)[:n]
     """
     from itertools import islice, count, tee, chain
+
     if n < 0:
         return []
     it = iter(iterable)
@@ -230,6 +249,7 @@ def nsmallest(n, iterable):
     result.sort()
     return result
 
+
 # 'heap' is a heap at all indices >= startpos, except possibly for pos.  pos
 # is the index of a leaf with a possibly out-of-order value.  Restore the
 # heap invariant.
@@ -247,6 +267,7 @@ def _siftdown(heap, startpos, pos):
         break
     heap[pos] = newitem
 
+
 # The child indices of heap index pos are already heaps, and we want to make
 # a heap at index pos too.  We do this by bubbling the smaller child of
 # pos up (and so on with that child's children, etc) until hitting a leaf,
@@ -286,12 +307,13 @@ def _siftdown(heap, startpos, pos):
 # heappop() compares):  list.sort() is (unsurprisingly!) more efficient
 # for sorting.
 
+
 def _siftup(heap, pos):
     endpos = len(heap)
     startpos = pos
     newitem = heap[pos]
     # Bubble up the smaller child until hitting a leaf.
-    childpos = 2*pos + 1    # leftmost child position
+    childpos = 2 * pos + 1  # leftmost child position
     while childpos < endpos:
         # Set childpos to index of smaller child.
         rightpos = childpos + 1
@@ -300,14 +322,15 @@ def _siftup(heap, pos):
         # Move the smaller child up.
         heap[pos] = heap[childpos]
         pos = childpos
-        childpos = 2*pos + 1
+        childpos = 2 * pos + 1
     # The leaf at pos is empty now.  Put newitem there, and bubble it up
     # to its final resting place (by sifting its parents down).
     heap[pos] = newitem
     _siftdown(heap, startpos, pos)
 
+
 def _siftdown_max(heap, startpos, pos):
-    'Maxheap variant of _siftdown'
+    "Maxheap variant of _siftdown"
     newitem = heap[pos]
     # Follow the path to the root, moving parents down until finding a place
     # newitem fits.
@@ -321,13 +344,14 @@ def _siftdown_max(heap, startpos, pos):
         break
     heap[pos] = newitem
 
+
 def _siftup_max(heap, pos):
-    'Maxheap variant of _siftup'
+    "Maxheap variant of _siftup"
     endpos = len(heap)
     startpos = pos
     newitem = heap[pos]
     # Bubble up the larger child until hitting a leaf.
-    childpos = 2*pos + 1    # leftmost child position
+    childpos = 2 * pos + 1  # leftmost child position
     while childpos < endpos:
         # Set childpos to index of larger child.
         rightpos = childpos + 1
@@ -336,20 +360,22 @@ def _siftup_max(heap, pos):
         # Move the larger child up.
         heap[pos] = heap[childpos]
         pos = childpos
-        childpos = 2*pos + 1
+        childpos = 2 * pos + 1
     # The leaf at pos is empty now.  Put newitem there, and bubble it up
     # to its final resting place (by sifting its parents down).
     heap[pos] = newitem
     _siftdown_max(heap, startpos, pos)
 
+
 # If available, use C implementation
 try:
     from _heapq import *
 except ImportError:
     pass
 
+
 def merge(*iterables):
-    '''Merge multiple sorted inputs into a single sorted output.
+    """Merge multiple sorted inputs into a single sorted output.
 
     Similar to sorted(itertools.chain(*iterables)) but returns a generator,
     does not pull the data into memory all at once, and assumes that each of
@@ -358,7 +384,7 @@ def merge(*iterables):
     >>> list(merge([1,3,5,7], [0,2,4,8], [5,10,15,20], [], [25]))
     [0, 1, 2, 3, 4, 5, 5, 7, 8, 10, 15, 20, 25]
 
-    '''
+    """
     _heappop, _heapreplace, _StopIteration = heappop, heapreplace, StopIteration
     _len = len
 
@@ -377,24 +403,28 @@ def merge(*iterables):
             while True:
                 v, itnum, next = s = h[0]
                 yield v
-                s[0] = next()               # raises StopIteration when exhausted
-                _heapreplace(h, s)          # restore heap condition
+                s[0] = next()  # raises StopIteration when exhausted
+                _heapreplace(h, s)  # restore heap condition
         except _StopIteration:
-            _heappop(h)                     # remove empty iterator
+            _heappop(h)  # remove empty iterator
     if h:
         # fast case when only a single iterator remains
         v, itnum, next = h[0]
         yield v
         yield from next.__self__
 
+
 # Extend the implementations of nsmallest and nlargest to use a key= argument
 _nsmallest = nsmallest
+
+
 def nsmallest(n, iterable, key=None):
     """Find the n smallest elements in a dataset.
 
     Equivalent to:  sorted(iterable, key=key)[:n]
     """
     from itertools import islice, count, tee, chain
+
     # Short-cut for n==1 is to use min() when len(iterable)>0
     if n == 1:
         it = iter(iterable)
@@ -416,17 +446,20 @@ def nsmallest(n, iterable, key=None):
 
     # When key is none, use simpler decoration
     if key is None:
-        it = zip(iterable, count())                         # decorate
+        it = zip(iterable, count())  # decorate
         result = _nsmallest(n, it)
-        return [r[0] for r in result]                       # undecorate
+        return [r[0] for r in result]  # undecorate
 
     # General case, slowest method
     in1, in2 = tee(iterable)
-    it = zip(map(key, in1), count(), in2)                   # decorate
+    it = zip(map(key, in1), count(), in2)  # decorate
     result = _nsmallest(n, it)
-    return [r[2] for r in result]                           # undecorate
+    return [r[2] for r in result]  # undecorate
+
 
 _nlargest = nlargest
+
+
 def nlargest(n, iterable, key=None):
     """Find the n largest elements in a dataset.
 
@@ -434,6 +467,7 @@ def nlargest(n, iterable, key=None):
     """
 
     from itertools import islice, count, tee, chain
+
     # Short-cut for n==1 is to use max() when len(iterable)>0
     if n == 1:
         it = iter(iterable)
@@ -455,15 +489,16 @@ def nlargest(n, iterable, key=None):
 
     # When key is none, use simpler decoration
     if key is None:
-        it = zip(iterable, count(0,-1))                     # decorate
+        it = zip(iterable, count(0, -1))  # decorate
         result = _nlargest(n, it)
-        return [r[0] for r in result]                       # undecorate
+        return [r[0] for r in result]  # undecorate
 
     # General case, slowest method
     in1, in2 = tee(iterable)
-    it = zip(map(key, in1), count(0,-1), in2)               # decorate
+    it = zip(map(key, in1), count(0, -1), in2)  # decorate
     result = _nlargest(n, it)
-    return [r[2] for r in result]                           # undecorate
+    return [r[2] for r in result]  # undecorate
+
 
 if __name__ == "__main__":
     # Simple sanity test
@@ -477,4 +512,5 @@ def nlargest(n, iterable, key=None):
     print(sort)
 
     import doctest
+
     doctest.testmod()
diff --git a/python-stdlib/heapq/manifest.py b/python-stdlib/heapq/manifest.py
new file mode 100644
index 000000000..1d71a3180
--- /dev/null
+++ b/python-stdlib/heapq/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.9.3")
+
+module("heapq.py")
diff --git a/heapq/test_heapq.py b/python-stdlib/heapq/test_heapq.py
similarity index 100%
rename from heapq/test_heapq.py
rename to python-stdlib/heapq/test_heapq.py
diff --git a/python-stdlib/hmac/hmac.py b/python-stdlib/hmac/hmac.py
new file mode 100644
index 000000000..dbbdd4718
--- /dev/null
+++ b/python-stdlib/hmac/hmac.py
@@ -0,0 +1,87 @@
+# Implements the hmac module from the Python standard library.
+
+
+class HMAC:
+    def __init__(self, key, msg=None, digestmod=None):
+        if not isinstance(key, (bytes, bytearray)):
+            raise TypeError("key: expected bytes/bytearray")
+
+        import hashlib
+
+        if digestmod is None:
+            # TODO: Default hash algorithm is now deprecated.
+            digestmod = hashlib.md5
+
+        if callable(digestmod):
+            # A hashlib constructor returning a new hash object.
+            make_hash = digestmod  # A
+        elif isinstance(digestmod, str):
+            # A hash name suitable for hashlib.new().
+            make_hash = lambda d=b"": getattr(hashlib, digestmod)(d)
+        else:
+            # A module supporting PEP 247.
+            make_hash = digestmod.new  # C
+
+        self._outer = make_hash()
+        self._inner = make_hash()
+
+        self.digest_size = getattr(self._inner, "digest_size", None)
+        # If the provided hash doesn't support block_size (e.g. built-in
+        # hashlib), 64 is the correct default for all built-in hash
+        # functions (md5, sha1, sha256).
+        self.block_size = getattr(self._inner, "block_size", 64)
+
+        # Truncate to digest_size if greater than block_size.
+        if len(key) > self.block_size:
+            key = make_hash(key).digest()
+
+        # Pad to block size.
+        key = key + bytes(self.block_size - len(key))
+
+        self._outer.update(bytes(x ^ 0x5C for x in key))
+        self._inner.update(bytes(x ^ 0x36 for x in key))
+
+        if msg is not None:
+            self.update(msg)
+
+    @property
+    def name(self):
+        return "hmac-" + getattr(self._inner, "name", type(self._inner).__name__)
+
+    def update(self, msg):
+        self._inner.update(msg)
+
+    def copy(self):
+        if not hasattr(self._inner, "copy"):
+            # Not supported for built-in hash functions.
+            raise NotImplementedError()
+        # Call __new__ directly to avoid the expensive __init__.
+        other = self.__class__.__new__(self.__class__)
+        other.block_size = self.block_size
+        other.digest_size = self.digest_size
+        other._inner = self._inner.copy()
+        other._outer = self._outer.copy()
+        return other
+
+    def _current(self):
+        h = self._outer
+        if hasattr(h, "copy"):
+            # built-in hash functions don't support this, and as a result,
+            # digest() will finalise the hmac and further calls to
+            # update/digest will fail.
+            h = h.copy()
+        h.update(self._inner.digest())
+        return h
+
+    def digest(self):
+        h = self._current()
+        return h.digest()
+
+    def hexdigest(self):
+        import binascii
+
+        return str(binascii.hexlify(self.digest()), "utf-8")
+
+
+def new(key, msg=None, digestmod=None):
+    return HMAC(key, msg, digestmod)
diff --git a/python-stdlib/hmac/manifest.py b/python-stdlib/hmac/manifest.py
new file mode 100644
index 000000000..ff0a62f08
--- /dev/null
+++ b/python-stdlib/hmac/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="3.4.4")
+
+module("hmac.py")
diff --git a/python-stdlib/hmac/test_hmac.py b/python-stdlib/hmac/test_hmac.py
new file mode 100644
index 000000000..1cfcf4e37
--- /dev/null
+++ b/python-stdlib/hmac/test_hmac.py
@@ -0,0 +1,59 @@
+import hmac
+
+# Uncomment to use micropython-lib hashlib (supports sha512)
+# import sys
+# sys.path.append('../hashlib')
+
+import hashlib
+
+msg = b"zlutoucky kun upel dabelske ody"
+
+dig = hmac.new(b"1234567890", msg=msg, digestmod="sha256").hexdigest()
+
+print("c735e751e36b08fb01e25794bdb15e7289b82aecdb652c8f4f72f307b39dad39")
+print(dig)
+
+if dig != "c735e751e36b08fb01e25794bdb15e7289b82aecdb652c8f4f72f307b39dad39":
+    raise Exception("Error")
+
+if hasattr(hashlib, "sha512"):
+    dig = hmac.new(b"1234567890", msg=msg, digestmod=hashlib.sha512).hexdigest()
+
+    print(
+        "59942f31b6f5473fb4eb630fabf5358a49bc11d24ebc83b114b4af30d6ef47ea14b673f478586f520a0b9c53b27c8f8dd618c165ef586195bd4e98293d34df1a"
+    )
+    print(dig)
+
+    if (
+        dig
+        != "59942f31b6f5473fb4eb630fabf5358a49bc11d24ebc83b114b4af30d6ef47ea14b673f478586f520a0b9c53b27c8f8dd618c165ef586195bd4e98293d34df1a"
+    ):
+        raise Exception("Error")
+else:
+    print("sha512 not supported")
+
+key = b"\x06\x1au\x90|Xz;o\x1b<\xafGL\xbfn\x8a\xc94YPfC^\xb9\xdd)\x7f\xaf\x85\xa1\xed\x82\xbexp\xaf\x13\x1a\x9d"
+
+dig = hmac.new(key[:20], msg=msg, digestmod=hashlib.sha256).hexdigest()
+
+print("59e332b881df09fdecf569c8b142b27fc989638720aeda2813f82442b6e3d91b")
+print(dig)
+
+if dig != "59e332b881df09fdecf569c8b142b27fc989638720aeda2813f82442b6e3d91b":
+    raise Exception("Error")
+
+dig = hmac.new(key[:32], msg=msg, digestmod=hashlib.sha256).hexdigest()
+
+print("b72fed815cd71acfa3a2f5cf2343679565fa18e7cd92226ab443aabd1fd7b7b0")
+print(dig)
+
+if dig != "b72fed815cd71acfa3a2f5cf2343679565fa18e7cd92226ab443aabd1fd7b7b0":
+    raise Exception("Error")
+
+dig = hmac.new(key, msg=msg, digestmod=hashlib.sha256).hexdigest()
+
+print("4e51beae6c2b0f90bb3e99d8e93a32d168b6c1e9b7d2130e2d668a3b3e10358d")
+print(dig)
+
+if dig != "4e51beae6c2b0f90bb3e99d8e93a32d168b6c1e9b7d2130e2d668a3b3e10358d":
+    raise Exception("Error")
diff --git a/python-stdlib/html/html/__init__.py b/python-stdlib/html/html/__init__.py
new file mode 100644
index 000000000..6fae53d1d
--- /dev/null
+++ b/python-stdlib/html/html/__init__.py
@@ -0,0 +1,29 @@
+"""
+General functions for HTML manipulation.
+"""
+
+
+_escape_map = {ord("&"): "&amp;", ord("<"): "&lt;", ord(">"): "&gt;"}
+_escape_map_full = {
+    ord("&"): "&amp;",
+    ord("<"): "&lt;",
+    ord(">"): "&gt;",
+    ord('"'): "&quot;",
+    ord("'"): "&#x27;",
+}
+
+# NB: this is a candidate for a bytes/string polymorphic interface
+
+
+def escape(s, quote=True):
+    """
+    Replace special characters "&", "<" and ">" to HTML-safe sequences.
+    If the optional flag quote is true (the default), the quotation mark
+    characters, both double quote (") and single quote (') characters are also
+    translated.
+    """
+    import string
+
+    if quote:
+        return string.translate(s, _escape_map_full)
+    return string.translate(s, _escape_map)
diff --git a/python-stdlib/html/manifest.py b/python-stdlib/html/manifest.py
new file mode 100644
index 000000000..c5705dd4b
--- /dev/null
+++ b/python-stdlib/html/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="3.3.4")
+
+require("string")
+
+package("html")
diff --git a/inspect/inspect.py b/python-stdlib/inspect/inspect.py
similarity index 77%
rename from inspect/inspect.py
rename to python-stdlib/inspect/inspect.py
index 57a7ab73a..c16c6b3e3 100644
--- a/inspect/inspect.py
+++ b/python-stdlib/inspect/inspect.py
@@ -1,5 +1,7 @@
 import sys
 
+_g = lambda: (yield)
+
 
 def getmembers(obj, pred=None):
     res = []
@@ -10,25 +12,40 @@ def getmembers(obj, pred=None):
     res.sort()
     return res
 
+
 def isfunction(obj):
     return isinstance(obj, type(isfunction))
 
+
 def isgeneratorfunction(obj):
-    return isinstance(obj, type(lambda:(yield)))
+    return isinstance(obj, type(_g))
+
 
 def isgenerator(obj):
-    return isinstance(obj, type(lambda:(yield)()))
+    return isinstance(obj, type((_g)()))
+
+
+# In MicroPython there's currently no way to distinguish between generators and coroutines.
+iscoroutinefunction = isgeneratorfunction
+iscoroutine = isgenerator
+
 
 class _Class:
-    def meth(): pass
+    def meth():
+        pass
+
+
 _Instance = _Class()
 
+
 def ismethod(obj):
     return isinstance(obj, type(_Instance.meth))
 
+
 def isclass(obj):
     return isinstance(obj, type)
 
+
 def ismodule(obj):
     return isinstance(obj, type(sys))
 
@@ -36,18 +53,23 @@ def ismodule(obj):
 def getargspec(func):
     raise NotImplementedError("This is over-dynamic function, not supported by MicroPython")
 
+
 def getmodule(obj, _filename=None):
     return None  # Not known
 
+
 def getmro(cls):
     return [cls]
 
+
 def getsourcefile(obj):
     return None  # Not known
 
+
 def getfile(obj):
     return "<unknown>"
 
+
 def getsource(obj):
     return "<source redacted to save you memory>"
 
@@ -55,5 +77,6 @@ def getsource(obj):
 def currentframe():
     return None
 
+
 def getframeinfo(frame, context=1):
     return ("<unknown>", -1, "<unknown>", [""], 0)
diff --git a/python-stdlib/inspect/manifest.py b/python-stdlib/inspect/manifest.py
new file mode 100644
index 000000000..e99e659f2
--- /dev/null
+++ b/python-stdlib/inspect/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.3")
+
+module("inspect.py")
diff --git a/python-stdlib/inspect/test_inspect.py b/python-stdlib/inspect/test_inspect.py
new file mode 100644
index 000000000..29ed80f11
--- /dev/null
+++ b/python-stdlib/inspect/test_inspect.py
@@ -0,0 +1,60 @@
+import inspect
+import unittest
+
+
+def fun():
+    return 1
+
+
+def gen():
+    yield 1
+
+
+class Class:
+    def meth(self):
+        pass
+
+
+entities = (
+    fun,
+    gen,
+    gen(),
+    Class,
+    Class.meth,
+    Class().meth,
+    inspect,
+)
+
+
+class TestInspect(unittest.TestCase):
+    def _test_is_helper(self, f, *entities_true):
+        for entity in entities:
+            result = f(entity)
+            if entity in entities_true:
+                self.assertTrue(result)
+            else:
+                self.assertFalse(result)
+
+    def test_isfunction(self):
+        self._test_is_helper(inspect.isfunction, entities[0], entities[4])
+
+    def test_isgeneratorfunction(self):
+        self._test_is_helper(inspect.isgeneratorfunction, entities[1])
+
+    def test_isgenerator(self):
+        self._test_is_helper(inspect.isgenerator, entities[2])
+
+    def test_iscoroutinefunction(self):
+        self._test_is_helper(inspect.iscoroutinefunction, entities[1])
+
+    def test_iscoroutine(self):
+        self._test_is_helper(inspect.iscoroutine, entities[2])
+
+    def test_ismethod(self):
+        self._test_is_helper(inspect.ismethod, entities[5])
+
+    def test_isclass(self):
+        self._test_is_helper(inspect.isclass, entities[3])
+
+    def test_ismodule(self):
+        self._test_is_helper(inspect.ismodule, entities[6])
diff --git a/python-stdlib/io/io.py b/python-stdlib/io/io.py
new file mode 100644
index 000000000..adc29544b
--- /dev/null
+++ b/python-stdlib/io/io.py
@@ -0,0 +1,5 @@
+from uio import *
+
+SEEK_SET = 0
+SEEK_CUR = 1
+SEEK_END = 2
diff --git a/python-stdlib/io/manifest.py b/python-stdlib/io/manifest.py
new file mode 100644
index 000000000..62c0a5147
--- /dev/null
+++ b/python-stdlib/io/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.0")
+
+module("io.py")
diff --git a/itertools/itertools.py b/python-stdlib/itertools/itertools.py
similarity index 82%
rename from itertools/itertools.py
rename to python-stdlib/itertools/itertools.py
index 5c00952ff..9bf1b2158 100644
--- a/itertools/itertools.py
+++ b/python-stdlib/itertools/itertools.py
@@ -3,6 +3,7 @@ def count(start=0, step=1):
         yield start
         start += step
 
+
 def cycle(p):
     try:
         len(p)
@@ -26,10 +27,12 @@ def repeat(el, n=None):
         for i in range(n):
             yield el
 
+
 def chain(*p):
     for i in p:
         yield from i
 
+
 def islice(p, start, stop=(), step=1):
     if stop == ():
         stop = start
@@ -49,9 +52,23 @@ def islice(p, start, stop=(), step=1):
         if start >= stop:
             return
 
+
 def tee(iterable, n=2):
     return [iter(iterable)] * n
 
+
 def starmap(function, iterable):
     for args in iterable:
         yield function(*args)
+
+
+def accumulate(iterable, func=lambda x, y: x + y):
+    it = iter(iterable)
+    try:
+        acc = next(it)
+    except StopIteration:
+        return
+    yield acc
+    for element in it:
+        acc = func(acc, element)
+        yield acc
diff --git a/python-stdlib/itertools/manifest.py b/python-stdlib/itertools/manifest.py
new file mode 100644
index 000000000..80ebd5024
--- /dev/null
+++ b/python-stdlib/itertools/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.2.3")
+
+module("itertools.py")
diff --git a/python-stdlib/itertools/test_itertools.py b/python-stdlib/itertools/test_itertools.py
new file mode 100644
index 000000000..bd16ce0be
--- /dev/null
+++ b/python-stdlib/itertools/test_itertools.py
@@ -0,0 +1,24 @@
+import itertools
+
+assert list(itertools.islice(list(range(10)), 4)) == [0, 1, 2, 3]
+assert list(itertools.islice(list(range(10)), 2, 6)) == [2, 3, 4, 5]
+assert list(itertools.islice(list(range(10)), 2, 6, 2)) == [2, 4]
+
+
+def g():
+    while True:
+        yield 123
+
+
+assert list(itertools.islice(g(), 5)) == [123, 123, 123, 123, 123]
+
+assert list(itertools.islice(itertools.cycle([1, 2, 3]), 10)) == [1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
+assert list(itertools.islice(itertools.cycle(reversed([1, 2, 3])), 7)) == [3, 2, 1, 3, 2, 1, 3]
+
+assert list(itertools.starmap(lambda x, y: x * y, [[1, 2], [2, 3], [3, 4]])) == [2, 6, 12]
+
+assert list(itertools.accumulate([])) == []
+assert list(itertools.accumulate([0])) == [0]
+assert list(itertools.accumulate([0, 2, 3])) == [0, 2, 5]
+assert list(itertools.accumulate(reversed([0, 2, 3]))) == [3, 5, 5]
+assert list(itertools.accumulate([1, 2, 3], lambda x, y: x * y)) == [1, 2, 6]
diff --git a/python-stdlib/keyword/keyword.py b/python-stdlib/keyword/keyword.py
new file mode 100644
index 000000000..8e9a16edb
--- /dev/null
+++ b/python-stdlib/keyword/keyword.py
@@ -0,0 +1,98 @@
+#! /usr/bin/env python3
+
+"""Keywords (from "graminit.c")
+
+This file is automatically generated; please don't muck it up!
+
+To update the symbols in this file, 'cd' to the top directory of
+the python source tree after building the interpreter and run:
+
+    ./python Lib/keyword.py
+"""
+
+__all__ = ["iskeyword", "kwlist"]
+
+kwlist = [
+    # --start keywords--
+    "False",
+    "None",
+    "True",
+    "and",
+    "as",
+    "assert",
+    "break",
+    "class",
+    "continue",
+    "def",
+    "del",
+    "elif",
+    "else",
+    "except",
+    "finally",
+    "for",
+    "from",
+    "global",
+    "if",
+    "import",
+    "in",
+    "is",
+    "lambda",
+    "nonlocal",
+    "not",
+    "or",
+    "pass",
+    "raise",
+    "return",
+    "try",
+    "while",
+    "with",
+    "yield",
+    # --end keywords--
+]
+
+frozenset = set
+iskeyword = frozenset(kwlist).__contains__
+
+
+def main():
+    import sys, re
+
+    args = sys.argv[1:]
+    iptfile = args and args[0] or "Python/graminit.c"
+    if len(args) > 1:
+        optfile = args[1]
+    else:
+        optfile = "Lib/keyword.py"
+
+    # scan the source file for keywords
+    with open(iptfile) as fp:
+        strprog = re.compile('"([^"]+)"')
+        lines = []
+        for line in fp:
+            if '{1, "' in line:
+                match = strprog.search(line)
+                if match:
+                    lines.append("        '" + match.group(1) + "',\n")
+    lines.sort()
+
+    # load the output skeleton from the target
+    with open(optfile) as fp:
+        format = fp.readlines()
+
+    # insert the lines of keywords
+    try:
+        start = format.index("#--start keywords--\n") + 1
+        end = format.index("#--end keywords--\n")
+        format[start:end] = lines
+    except ValueError:
+        sys.stderr.write("target does not contain format markers\n")
+        sys.exit(1)
+
+    # write the output file
+    fp = open(optfile, "w")
+    fp.write("".join(format))
+    fp.close()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/python-stdlib/keyword/manifest.py b/python-stdlib/keyword/manifest.py
new file mode 100644
index 000000000..aad27ec89
--- /dev/null
+++ b/python-stdlib/keyword/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.0.1")
+
+module("keyword.py")
diff --git a/locale/locale.py b/python-stdlib/locale/locale.py
similarity index 100%
rename from locale/locale.py
rename to python-stdlib/locale/locale.py
diff --git a/python-stdlib/locale/manifest.py b/python-stdlib/locale/manifest.py
new file mode 100644
index 000000000..9d56ae6a5
--- /dev/null
+++ b/python-stdlib/locale/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.0.2")
+
+module("locale.py")
diff --git a/python-stdlib/logging/examples/basic_example.py b/python-stdlib/logging/examples/basic_example.py
new file mode 100644
index 000000000..002bc3726
--- /dev/null
+++ b/python-stdlib/logging/examples/basic_example.py
@@ -0,0 +1,7 @@
+import logging
+
+logging.debug("test - debug")  # ignored by default
+logging.info("test - info")  # ignored by default
+logging.warning("test - warning")
+logging.error("test - error")
+logging.critical("test - critical")
diff --git a/python-stdlib/logging/examples/example_logging_1.py b/python-stdlib/logging/examples/example_logging_1.py
new file mode 100644
index 000000000..e81308859
--- /dev/null
+++ b/python-stdlib/logging/examples/example_logging_1.py
@@ -0,0 +1,24 @@
+import logging
+
+logging.basicConfig(level=logging.INFO)
+log = logging.getLogger("test")
+log.debug("Test message: %d(%s)", 100, "foobar")
+log.info("Test message2: %d(%s)", 100, "foobar")
+log.warning("Test message3: %d(%s)")
+log.error("Test message4")
+log.critical("Test message5")
+logging.info("Test message6")
+
+try:
+    1 / 0
+except:
+    log.exception("Some trouble (%s)", "expected")
+
+
+class MyHandler(logging.Handler):
+    def emit(self, record):
+        print("levelname=%(levelname)s name=%(name)s message=%(message)s" % record.__dict__)
+
+
+logging.getLogger().addHandler(MyHandler())
+logging.info("Test message7")
diff --git a/python-stdlib/logging/examples/example_logging_2.py b/python-stdlib/logging/examples/example_logging_2.py
new file mode 100644
index 000000000..1dba328bc
--- /dev/null
+++ b/python-stdlib/logging/examples/example_logging_2.py
@@ -0,0 +1,48 @@
+import logging
+
+# Create logger
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+# Create console handler and set level to debug
+stream_handler = logging.StreamHandler()
+stream_handler.setLevel(logging.DEBUG)
+
+# Create file handler and set level to error
+file_handler = logging.FileHandler("error.log", mode="w")
+file_handler.setLevel(logging.ERROR)
+
+# Create a formatter
+formatter = logging.Formatter("%(asctime)s.%(msecs)03d - %(name)s - %(levelname)s - %(message)s")
+
+# Add formatter to the handlers
+stream_handler.setFormatter(formatter)
+file_handler.setFormatter(formatter)
+
+# Add handlers to logger
+logger.addHandler(stream_handler)
+logger.addHandler(file_handler)
+
+# Log some messages
+logger.debug("debug message")
+logger.info("info message")
+logger.warning("warn message")
+logger.error("error message")
+logger.critical("critical message")
+logger.info("message %s %d", "arg", 5)
+logger.info("message %(foo)s %(bar)s", {"foo": 1, "bar": 20})
+
+try:
+    1 / 0
+except:
+    logger.error("Some trouble (%s)", "expected")
+
+
+# Custom handler example
+class MyHandler(logging.Handler):
+    def emit(self, record):
+        print("levelname=%(levelname)s name=%(name)s message=%(message)s" % record.__dict__)
+
+
+logging.getLogger().addHandler(MyHandler())
+logging.info("Test message7")
diff --git a/python-stdlib/logging/examples/root_logger.py b/python-stdlib/logging/examples/root_logger.py
new file mode 100644
index 000000000..6e118ce39
--- /dev/null
+++ b/python-stdlib/logging/examples/root_logger.py
@@ -0,0 +1,8 @@
+import logging, sys
+
+logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
+for handler in logging.getLogger().handlers:
+    handler.setFormatter(logging.Formatter("[%(levelname)s]:%(name)s:%(message)s"))
+logging.info("hello upy")
+logging.getLogger("child").info("hello 2")
+logging.getLogger("child").debug("hello 2")
diff --git a/python-stdlib/logging/logging.py b/python-stdlib/logging/logging.py
new file mode 100644
index 000000000..551bf7152
--- /dev/null
+++ b/python-stdlib/logging/logging.py
@@ -0,0 +1,253 @@
+from micropython import const
+import io
+import sys
+import time
+
+CRITICAL = const(50)
+ERROR = const(40)
+WARNING = const(30)
+INFO = const(20)
+DEBUG = const(10)
+NOTSET = const(0)
+
+_DEFAULT_LEVEL = const(WARNING)
+
+_level_dict = {
+    CRITICAL: "CRITICAL",
+    ERROR: "ERROR",
+    WARNING: "WARNING",
+    INFO: "INFO",
+    DEBUG: "DEBUG",
+    NOTSET: "NOTSET",
+}
+
+_loggers = {}
+_stream = sys.stderr
+_default_fmt = "%(levelname)s:%(name)s:%(message)s"
+_default_datefmt = "%Y-%m-%d %H:%M:%S"
+
+
+class LogRecord:
+    def set(self, name, level, message):
+        self.name = name
+        self.levelno = level
+        self.levelname = _level_dict[level]
+        self.message = message
+        self.ct = time.time()
+        self.msecs = int((self.ct - int(self.ct)) * 1000)
+        self.asctime = None
+
+
+class Handler:
+    def __init__(self, level=NOTSET):
+        self.level = level
+        self.formatter = None
+
+    def close(self):
+        pass
+
+    def setLevel(self, level):
+        self.level = level
+
+    def setFormatter(self, formatter):
+        self.formatter = formatter
+
+    def format(self, record):
+        return self.formatter.format(record)
+
+
+class StreamHandler(Handler):
+    def __init__(self, stream=None):
+        super().__init__()
+        self.stream = _stream if stream is None else stream
+        self.terminator = "\n"
+
+    def close(self):
+        if hasattr(self.stream, "flush"):
+            self.stream.flush()
+
+    def emit(self, record):
+        if record.levelno >= self.level:
+            self.stream.write(self.format(record) + self.terminator)
+
+
+class FileHandler(StreamHandler):
+    def __init__(self, filename, mode="a", encoding="UTF-8"):
+        super().__init__(stream=open(filename, mode=mode, encoding=encoding))
+
+    def close(self):
+        super().close()
+        self.stream.close()
+
+
+class Formatter:
+    def __init__(self, fmt=None, datefmt=None):
+        self.fmt = _default_fmt if fmt is None else fmt
+        self.datefmt = _default_datefmt if datefmt is None else datefmt
+
+    def usesTime(self):
+        return "asctime" in self.fmt
+
+    def formatTime(self, datefmt, record):
+        if hasattr(time, "strftime"):
+            return time.strftime(datefmt, time.localtime(record.ct))
+        return None
+
+    def format(self, record):
+        if self.usesTime():
+            record.asctime = self.formatTime(self.datefmt, record)
+        return self.fmt % {
+            "name": record.name,
+            "message": record.message,
+            "msecs": record.msecs,
+            "asctime": record.asctime,
+            "levelname": record.levelname,
+        }
+
+
+class Logger:
+    def __init__(self, name, level=NOTSET):
+        self.name = name
+        self.level = level
+        self.handlers = []
+        self.record = LogRecord()
+
+    def setLevel(self, level):
+        self.level = level
+
+    def isEnabledFor(self, level):
+        return level >= self.getEffectiveLevel()
+
+    def getEffectiveLevel(self):
+        return self.level or getLogger().level or _DEFAULT_LEVEL
+
+    def log(self, level, msg, *args):
+        if self.isEnabledFor(level):
+            if args:
+                if isinstance(args[0], dict):
+                    args = args[0]
+                msg = msg % args
+            self.record.set(self.name, level, msg)
+            handlers = self.handlers
+            if not handlers:
+                handlers = getLogger().handlers
+            for h in handlers:
+                h.emit(self.record)
+
+    def debug(self, msg, *args):
+        self.log(DEBUG, msg, *args)
+
+    def info(self, msg, *args):
+        self.log(INFO, msg, *args)
+
+    def warning(self, msg, *args):
+        self.log(WARNING, msg, *args)
+
+    def error(self, msg, *args):
+        self.log(ERROR, msg, *args)
+
+    def critical(self, msg, *args):
+        self.log(CRITICAL, msg, *args)
+
+    def exception(self, msg, *args, exc_info=True):
+        self.log(ERROR, msg, *args)
+        tb = None
+        if isinstance(exc_info, BaseException):
+            tb = exc_info
+        elif hasattr(sys, "exc_info"):
+            tb = sys.exc_info()[1]
+        if tb:
+            buf = io.StringIO()
+            sys.print_exception(tb, buf)
+            self.log(ERROR, buf.getvalue())
+
+    def addHandler(self, handler):
+        self.handlers.append(handler)
+
+    def hasHandlers(self):
+        return len(self.handlers) > 0
+
+
+def getLogger(name=None):
+    if name is None:
+        name = "root"
+    if name not in _loggers:
+        _loggers[name] = Logger(name)
+        if name == "root":
+            basicConfig()
+    return _loggers[name]
+
+
+def log(level, msg, *args):
+    getLogger().log(level, msg, *args)
+
+
+def debug(msg, *args):
+    getLogger().debug(msg, *args)
+
+
+def info(msg, *args):
+    getLogger().info(msg, *args)
+
+
+def warning(msg, *args):
+    getLogger().warning(msg, *args)
+
+
+def error(msg, *args):
+    getLogger().error(msg, *args)
+
+
+def critical(msg, *args):
+    getLogger().critical(msg, *args)
+
+
+def exception(msg, *args, exc_info=True):
+    getLogger().exception(msg, *args, exc_info=exc_info)
+
+
+def shutdown():
+    for k, logger in _loggers.items():
+        for h in logger.handlers:
+            h.close()
+        _loggers.pop(logger, None)
+
+
+def addLevelName(level, name):
+    _level_dict[level] = name
+
+
+def basicConfig(
+    filename=None,
+    filemode="a",
+    format=None,
+    datefmt=None,
+    level=WARNING,
+    stream=None,
+    encoding="UTF-8",
+    force=False,
+):
+    if "root" not in _loggers:
+        _loggers["root"] = Logger("root")
+
+    logger = _loggers["root"]
+
+    if force or not logger.handlers:
+        for h in logger.handlers:
+            h.close()
+        logger.handlers = []
+
+        if filename is None:
+            handler = StreamHandler(stream)
+        else:
+            handler = FileHandler(filename, filemode, encoding)
+
+        handler.setLevel(level)
+        handler.setFormatter(Formatter(format, datefmt))
+
+        logger.setLevel(level)
+        logger.addHandler(handler)
+
+
+if hasattr(sys, "atexit"):
+    sys.atexit(shutdown)
diff --git a/python-stdlib/logging/manifest.py b/python-stdlib/logging/manifest.py
new file mode 100644
index 000000000..c2614e9ea
--- /dev/null
+++ b/python-stdlib/logging/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.6.2")
+
+module("logging.py")
diff --git a/python-stdlib/operator/manifest.py b/python-stdlib/operator/manifest.py
new file mode 100644
index 000000000..0d53e597d
--- /dev/null
+++ b/python-stdlib/operator/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.1")
+
+module("operator.py")
diff --git a/python-stdlib/operator/operator.py b/python-stdlib/operator/operator.py
new file mode 100644
index 000000000..e20987395
--- /dev/null
+++ b/python-stdlib/operator/operator.py
@@ -0,0 +1,43 @@
+def attrgetter(attr):
+    assert "." not in attr
+
+    def _attrgetter(obj):
+        return getattr(obj, attr)
+
+    return _attrgetter
+
+
+def lt(a, b):
+    return a < b
+
+
+def le(a, b):
+    return a <= b
+
+
+def gt(a, b):
+    return a > b
+
+
+def ge(a, b):
+    return a >= b
+
+
+def eq(a, b):
+    return a == b
+
+
+def ne(a, b):
+    return a != b
+
+
+def mod(a, b):
+    return a % b
+
+
+def truediv(a, b):
+    return a / b
+
+
+def floordiv(a, b):
+    return a // b
diff --git a/python-stdlib/operator/test_operator.py b/python-stdlib/operator/test_operator.py
new file mode 100644
index 000000000..262f1c227
--- /dev/null
+++ b/python-stdlib/operator/test_operator.py
@@ -0,0 +1,11 @@
+import operator
+
+
+class A:
+    pass
+
+
+a = A()
+a.name = "foo"
+f = operator.attrgetter("name")
+assert f(a) == "foo"
diff --git a/python-stdlib/os-path/manifest.py b/python-stdlib/os-path/manifest.py
new file mode 100644
index 000000000..4433e6a4d
--- /dev/null
+++ b/python-stdlib/os-path/manifest.py
@@ -0,0 +1,6 @@
+metadata(version="0.2.0")
+
+# Originally written by Paul Sokolovsky.
+
+require("os")
+package("os")
diff --git a/os.path/os/path.py b/python-stdlib/os-path/os/path.py
similarity index 77%
rename from os.path/os/path.py
rename to python-stdlib/os-path/os/path.py
index 5844693ab..b9ae1972f 100644
--- a/os.path/os/path.py
+++ b/python-stdlib/os-path/os/path.py
@@ -3,17 +3,21 @@
 
 sep = "/"
 
+
 def normcase(s):
     return s
 
+
 def normpath(s):
     return s
 
+
 def abspath(s):
     if s[0] != "/":
         return os.getcwd() + "/" + s
     return s
 
+
 def join(*args):
     # TODO: this is non-compliant
     if type(args[0]) is bytes:
@@ -21,34 +25,50 @@ def join(*args):
     else:
         return "/".join(args)
 
+
 def split(path):
     if path == "":
         return ("", "")
     r = path.rsplit("/", 1)
     if len(r) == 1:
         return ("", path)
-    head = r[0] #.rstrip("/")
+    head = r[0]  # .rstrip("/")
     if not head:
         head = "/"
     return (head, r[1])
 
+
 def dirname(path):
     return split(path)[0]
 
+
 def basename(path):
     return split(path)[1]
 
+
 def exists(path):
-    return os.access(path, os.F_OK)
+    try:
+        os.stat(path)
+        return True
+    except OSError:
+        return False
+
 
 # TODO
 lexists = exists
 
+
 def isdir(path):
-    import stat
     try:
         mode = os.stat(path)[0]
-        return stat.S_ISDIR(mode)
+        return mode & 0o040000
+    except OSError:
+        return False
+
+
+def isfile(path):
+    try:
+        return bool(os.stat(path)[0] & 0x8000)
     except OSError:
         return False
 
diff --git a/python-stdlib/os-path/test_path.py b/python-stdlib/os-path/test_path.py
new file mode 100644
index 000000000..85178364b
--- /dev/null
+++ b/python-stdlib/os-path/test_path.py
@@ -0,0 +1,26 @@
+import sys
+
+dir = "."
+if "/" in __file__:
+    dir = __file__.rsplit("/", 1)[0]
+
+sys.path[0] = dir + "/os"
+from path import *
+
+assert split("") == ("", "")
+assert split("path") == ("", "path")
+assert split("/") == ("/", "")
+assert split("/foo") == ("/", "foo")
+assert split("/foo/") == ("/foo", "")
+assert split("/foo/bar") == ("/foo", "bar")
+
+assert exists(dir + "/test_path.py")
+assert not exists(dir + "/test_path.py--")
+
+assert isdir(dir + "/os")
+assert not isdir(dir + "/os--")
+assert not isdir(dir + "/test_path.py")
+
+assert not isfile(dir + "/os")
+assert isfile(dir + "/test_path.py")
+assert not isfile(dir + "/test_path.py--")
diff --git a/python-stdlib/os/manifest.py b/python-stdlib/os/manifest.py
new file mode 100644
index 000000000..cd59f0c91
--- /dev/null
+++ b/python-stdlib/os/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.6.0")
+
+# Originally written by Paul Sokolovsky.
+
+package("os")
diff --git a/python-stdlib/os/os/__init__.py b/python-stdlib/os/os/__init__.py
new file mode 100644
index 000000000..6e51bd0d3
--- /dev/null
+++ b/python-stdlib/os/os/__init__.py
@@ -0,0 +1,8 @@
+# Replace built-in os module.
+from uos import *
+
+# Provide optional dependencies (which may be installed separately).
+try:
+    from . import path
+except ImportError:
+    pass
diff --git a/python-stdlib/pathlib/manifest.py b/python-stdlib/pathlib/manifest.py
new file mode 100644
index 000000000..37dcaf634
--- /dev/null
+++ b/python-stdlib/pathlib/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.0.1")
+
+module("pathlib.py")
diff --git a/python-stdlib/pathlib/pathlib.py b/python-stdlib/pathlib/pathlib.py
new file mode 100644
index 000000000..e0f961373
--- /dev/null
+++ b/python-stdlib/pathlib/pathlib.py
@@ -0,0 +1,210 @@
+import errno
+import os
+
+from micropython import const
+
+_SEP = const("/")
+
+
+def _mode_if_exists(path):
+    try:
+        return os.stat(path)[0]
+    except OSError as e:
+        if e.errno == errno.ENOENT:
+            return 0
+        raise e
+
+
+def _clean_segment(segment):
+    segment = str(segment)
+    if not segment:
+        return "."
+    segment = segment.rstrip(_SEP)
+    if not segment:
+        return _SEP
+    while True:
+        no_double = segment.replace(_SEP + _SEP, _SEP)
+        if no_double == segment:
+            break
+        segment = no_double
+    return segment
+
+
+class Path:
+    def __init__(self, *segments):
+        segments_cleaned = []
+        for segment in segments:
+            segment = _clean_segment(segment)
+            if segment[0] == _SEP:
+                segments_cleaned = [segment]
+            elif segment == ".":
+                continue
+            else:
+                segments_cleaned.append(segment)
+
+        self._path = _clean_segment(_SEP.join(segments_cleaned))
+
+    def __truediv__(self, other):
+        return Path(self._path, str(other))
+
+    def __rtruediv__(self, other):
+        return Path(other, self._path)
+
+    def __repr__(self):
+        return f'{type(self).__name__}("{self._path}")'
+
+    def __str__(self):
+        return self._path
+
+    def __eq__(self, other):
+        return self.absolute() == Path(other).absolute()
+
+    def absolute(self):
+        path = self._path
+        cwd = os.getcwd()
+        if not path or path == ".":
+            return cwd
+        if path[0] == _SEP:
+            return path
+        return _SEP + path if cwd == _SEP else cwd + _SEP + path
+
+    def resolve(self):
+        return self.absolute()
+
+    def open(self, mode="r", encoding=None):
+        return open(self._path, mode, encoding=encoding)
+
+    def exists(self):
+        return bool(_mode_if_exists(self._path))
+
+    def mkdir(self, parents=False, exist_ok=False):
+        try:
+            os.mkdir(self._path)
+            return
+        except OSError as e:
+            if e.errno == errno.EEXIST and exist_ok:
+                return
+            elif e.errno == errno.ENOENT and parents:
+                pass  # handled below
+            else:
+                raise e
+
+        segments = self._path.split(_SEP)
+        progressive_path = ""
+        if segments[0] == "":
+            segments = segments[1:]
+            progressive_path = _SEP
+        for segment in segments:
+            progressive_path += _SEP + segment
+            try:
+                os.mkdir(progressive_path)
+            except OSError as e:
+                if e.errno != errno.EEXIST:
+                    raise e
+
+    def is_dir(self):
+        return bool(_mode_if_exists(self._path) & 0x4000)
+
+    def is_file(self):
+        return bool(_mode_if_exists(self._path) & 0x8000)
+
+    def _glob(self, path, pattern, recursive):
+        # Currently only supports a single "*" pattern.
+        n_wildcards = pattern.count("*")
+        n_single_wildcards = pattern.count("?")
+
+        if n_single_wildcards:
+            raise NotImplementedError("? single wildcards not implemented.")
+
+        if n_wildcards == 0:
+            raise ValueError
+        elif n_wildcards > 1:
+            raise NotImplementedError("Multiple * wildcards not implemented.")
+
+        prefix, suffix = pattern.split("*")
+
+        for name, mode, *_ in os.ilistdir(path):
+            full_path = path + _SEP + name
+            if name.startswith(prefix) and name.endswith(suffix):
+                yield full_path
+            if recursive and mode & 0x4000:  # is_dir
+                yield from self._glob(full_path, pattern, recursive=recursive)
+
+    def glob(self, pattern):
+        """Iterate over this subtree and yield all existing files (of any
+        kind, including directories) matching the given relative pattern.
+
+        Currently only supports a single "*" pattern.
+        """
+        return self._glob(self._path, pattern, recursive=False)
+
+    def rglob(self, pattern):
+        return self._glob(self._path, pattern, recursive=True)
+
+    def stat(self):
+        return os.stat(self._path)
+
+    def read_bytes(self):
+        with open(self._path, "rb") as f:
+            return f.read()
+
+    def read_text(self, encoding=None):
+        with open(self._path, "r", encoding=encoding) as f:
+            return f.read()
+
+    def rename(self, target):
+        os.rename(self._path, target)
+
+    def rmdir(self):
+        os.rmdir(self._path)
+
+    def touch(self, exist_ok=True):
+        if self.exists():
+            if exist_ok:
+                return  # TODO: should update timestamp
+            else:
+                # In lieue of FileExistsError
+                raise OSError(errno.EEXIST)
+        with open(self._path, "w"):
+            pass
+
+    def unlink(self, missing_ok=False):
+        try:
+            os.unlink(self._path)
+        except OSError as e:
+            if not (missing_ok and e.errno == errno.ENOENT):
+                raise e
+
+    def write_bytes(self, data):
+        with open(self._path, "wb") as f:
+            f.write(data)
+
+    def write_text(self, data, encoding=None):
+        with open(self._path, "w", encoding=encoding) as f:
+            f.write(data)
+
+    def with_suffix(self, suffix):
+        index = -len(self.suffix) or None
+        return Path(self._path[:index] + suffix)
+
+    @property
+    def stem(self):
+        return self.name.rsplit(".", 1)[0]
+
+    @property
+    def parent(self):
+        tokens = self._path.rsplit(_SEP, 1)
+        if len(tokens) == 2:
+            if not tokens[0]:
+                tokens[0] = _SEP
+            return Path(tokens[0])
+        return Path(".")
+
+    @property
+    def name(self):
+        return self._path.rsplit(_SEP, 1)[-1]
+
+    @property
+    def suffix(self):
+        elems = self._path.rsplit(".", 1)
+        return "" if len(elems) == 1 else "." + elems[1]
diff --git a/python-stdlib/pathlib/tests/test_pathlib.py b/python-stdlib/pathlib/tests/test_pathlib.py
new file mode 100644
index 000000000..e632e1242
--- /dev/null
+++ b/python-stdlib/pathlib/tests/test_pathlib.py
@@ -0,0 +1,335 @@
+import os
+import unittest
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+
+def _isgenerator(x):
+    return isinstance(x, type((lambda: (yield))()))
+
+
+class TestPathlib(unittest.TestCase):
+    def assertExists(self, fn):
+        os.stat(fn)
+
+    def assertNotExists(self, fn):
+        with self.assertRaises(OSError):
+            os.stat(fn)
+
+    def setUp(self):
+        self._tmp_path_obj = TemporaryDirectory()
+        self.tmp_path = self._tmp_path_obj.name
+
+    def tearDown(self):
+        self._tmp_path_obj.cleanup()
+
+    def test_init_single_segment(self):
+        path = Path("foo")
+        self.assertTrue(path._path == "foo")
+
+        path = Path("foo/")
+        self.assertTrue(path._path == "foo")
+
+        path = Path("/foo")
+        self.assertTrue(path._path == "/foo")
+
+        path = Path("/////foo")
+        self.assertTrue(path._path == "/foo")
+
+        path = Path("")
+        self.assertTrue(path._path == ".")
+
+    def test_init_multiple_segment(self):
+        path = Path("foo", "bar")
+        self.assertTrue(path._path == "foo/bar")
+
+        path = Path("foo/", "bar")
+        self.assertTrue(path._path == "foo/bar")
+
+        path = Path("/foo", "bar")
+        self.assertTrue(path._path == "/foo/bar")
+
+        path = Path("/foo", "", "bar")
+        self.assertTrue(path._path == "/foo/bar")
+
+        path = Path("/foo/", "", "/bar/")
+        self.assertTrue(path._path == "/bar")
+
+        path = Path("", "")
+        self.assertTrue(path._path == ".")
+
+    def test_truediv_join_str(self):
+        actual = Path("foo") / "bar"
+        self.assertTrue(actual == Path("foo/bar"))
+
+    def test_truediv_join_path(self):
+        actual = Path("foo") / Path("bar")
+        self.assertTrue(actual == Path("foo/bar"))
+
+        actual = Path("foo") / Path("/bar")
+        self.assertTrue(actual == "/bar")
+
+    def test_eq_and_absolute(self):
+        self.assertTrue(Path("") == Path("."))
+        self.assertTrue(Path("foo") == Path(os.getcwd(), "foo"))
+        self.assertTrue(Path("foo") == "foo")
+        self.assertTrue(Path("foo") == os.getcwd() + "/foo")
+
+        self.assertTrue(Path("foo") != Path("bar"))
+        self.assertTrue(Path(".") != Path("/"))
+
+    def test_open(self):
+        fn = self.tmp_path + "/foo.txt"
+        path = Path(fn)
+
+        with open(fn, "w") as f:
+            f.write("file contents")
+
+        with path.open("r") as f:
+            actual = f.read()
+
+        self.assertTrue(actual == "file contents")
+
+    def test_exists(self):
+        fn = self.tmp_path + "/foo.txt"
+
+        path = Path(str(fn))
+        self.assertTrue(not path.exists())
+
+        with open(fn, "w"):
+            pass
+
+        self.assertTrue(path.exists())
+
+    def test_mkdir(self):
+        target = self.tmp_path + "/foo/bar/baz"
+        path = Path(target)
+
+        with self.assertRaises(OSError):
+            path.mkdir()
+
+        with self.assertRaises(OSError):
+            path.mkdir(exist_ok=True)
+
+        path.mkdir(parents=True)
+        self.assertExists(target)
+
+        with self.assertRaises(OSError):
+            path.mkdir(exist_ok=False)
+
+        path.mkdir(exist_ok=True)
+
+    def test_is_dir(self):
+        target = self.tmp_path
+        path = Path(target)
+        self.assertTrue(path.is_dir())
+
+        target = self.tmp_path + "/foo"
+        path = Path(target)
+        self.assertTrue(not path.is_dir())
+        os.mkdir(target)
+        self.assertTrue(path.is_dir())
+
+        target = self.tmp_path + "/bar.txt"
+        path = Path(target)
+        self.assertTrue(not path.is_dir())
+        with open(target, "w"):
+            pass
+        self.assertTrue(not path.is_dir())
+
+    def test_is_file(self):
+        target = self.tmp_path
+        path = Path(target)
+        self.assertTrue(not path.is_file())
+
+        target = self.tmp_path + "/bar.txt"
+        path = Path(target)
+        self.assertTrue(not path.is_file())
+        with open(target, "w"):
+            pass
+        self.assertTrue(path.is_file())
+
+    def test_glob(self):
+        foo_txt = self.tmp_path + "/foo.txt"
+        with open(foo_txt, "w"):
+            pass
+        bar_txt = self.tmp_path + "/bar.txt"
+        with open(bar_txt, "w"):
+            pass
+        baz_bin = self.tmp_path + "/baz.bin"
+        with open(baz_bin, "w"):
+            pass
+
+        path = Path(self.tmp_path)
+        glob_gen = path.glob("*.txt")
+        self.assertTrue(_isgenerator(glob_gen))
+
+        res = [str(x) for x in glob_gen]
+        self.assertTrue(len(res) == 2)
+        self.assertTrue(foo_txt in res)
+        self.assertTrue(bar_txt in res)
+
+    def test_rglob(self):
+        foo_txt = self.tmp_path + "/foo.txt"
+        with open(foo_txt, "w"):
+            pass
+        bar_txt = self.tmp_path + "/bar.txt"
+        with open(bar_txt, "w"):
+            pass
+        baz_bin = self.tmp_path + "/baz.bin"
+        with open(baz_bin, "w"):
+            pass
+
+        boop_folder = self.tmp_path + "/boop"
+        os.mkdir(boop_folder)
+        bap_txt = self.tmp_path + "/boop/bap.txt"
+        with open(bap_txt, "w"):
+            pass
+
+        path = Path(self.tmp_path)
+        glob_gen = path.rglob("*.txt")
+        self.assertTrue(_isgenerator(glob_gen))
+
+        res = [str(x) for x in glob_gen]
+        self.assertTrue(len(res) == 3)
+        self.assertTrue(foo_txt in res)
+        self.assertTrue(bar_txt in res)
+        self.assertTrue(bap_txt in res)
+
+    def test_stat(self):
+        expected = os.stat(self.tmp_path)
+        path = Path(self.tmp_path)
+        actual = path.stat()
+        self.assertTrue(expected == actual)
+
+    def test_rmdir(self):
+        target = self.tmp_path + "/foo"
+        path = Path(target)
+
+        with self.assertRaises(OSError):
+            # Doesn't exist
+            path.rmdir()
+
+        os.mkdir(target)
+        self.assertExists(target)
+        path.rmdir()
+        self.assertNotExists(target)
+
+        os.mkdir(target)
+        with open(target + "/bar.txt", "w"):
+            pass
+
+        with self.assertRaises(OSError):
+            # Cannot rmdir; contains file.
+            path.rmdir()
+
+    def test_touch(self):
+        target = self.tmp_path + "/foo.txt"
+
+        path = Path(target)
+        path.touch()
+        self.assertExists(target)
+
+        path.touch()  # touching existing file is fine
+        self.assertExists(target)
+
+        # Technically should be FileExistsError,
+        # but thats not builtin to micropython
+        with self.assertRaises(OSError):
+            path.touch(exist_ok=False)
+
+        path = Path(self.tmp_path + "/bar/baz.txt")
+        with self.assertRaises(OSError):
+            # Parent directory does not exist
+            path.touch()
+
+    def test_unlink(self):
+        target = self.tmp_path + "/foo.txt"
+
+        path = Path(target)
+        with self.assertRaises(OSError):
+            # File does not exist
+            path.unlink()
+
+        with open(target, "w"):
+            pass
+
+        self.assertExists(target)
+        path.unlink()
+        self.assertNotExists(target)
+
+        path = Path(self.tmp_path)
+        with self.assertRaises(OSError):
+            # File does not exist
+            path.unlink()
+
+    def test_write_bytes(self):
+        target = self.tmp_path + "/foo.bin"
+        path = Path(target)
+        path.write_bytes(b"test byte data")
+        with open(target, "rb") as f:
+            actual = f.read()
+        self.assertTrue(actual == b"test byte data")
+
+    def test_write_text(self):
+        target = self.tmp_path + "/foo.txt"
+        path = Path(target)
+        path.write_text("test string")
+        with open(target, "r") as f:
+            actual = f.read()
+        self.assertTrue(actual == "test string")
+
+    def test_read_bytes(self):
+        target = self.tmp_path + "/foo.bin"
+        with open(target, "wb") as f:
+            f.write(b"test byte data")
+
+        path = Path(target)
+        actual = path.read_bytes()
+        self.assertTrue(actual == b"test byte data")
+
+    def test_read_text(self):
+        target = self.tmp_path + "/foo.bin"
+        with open(target, "w") as f:
+            f.write("test string")
+
+        path = Path(target)
+        actual = path.read_text()
+        self.assertTrue(actual == "test string")
+
+    def test_stem(self):
+        self.assertTrue(Path("foo/test").stem == "test")
+        self.assertTrue(Path("foo/bar.bin").stem == "bar")
+        self.assertTrue(Path("").stem == "")
+
+    def test_name(self):
+        self.assertTrue(Path("foo/test").name == "test")
+        self.assertTrue(Path("foo/bar.bin").name == "bar.bin")
+
+    def test_parent(self):
+        self.assertTrue(Path("foo/test").parent == Path("foo"))
+        self.assertTrue(Path("foo/bar.bin").parent == Path("foo"))
+        self.assertTrue(Path("bar.bin").parent == Path("."))
+        self.assertTrue(Path(".").parent == Path("."))
+        self.assertTrue(Path("/").parent == Path("/"))
+
+    def test_suffix(self):
+        self.assertTrue(Path("foo/test").suffix == "")
+        self.assertTrue(Path("foo/bar.bin").suffix == ".bin")
+        self.assertTrue(Path("bar.txt").suffix == ".txt")
+
+    def test_with_suffix(self):
+        self.assertTrue(Path("foo/test").with_suffix(".tar") == Path("foo/test.tar"))
+        self.assertTrue(Path("foo/bar.bin").with_suffix(".txt") == Path("foo/bar.txt"))
+        self.assertTrue(Path("bar.txt").with_suffix("") == Path("bar"))
+
+    def test_rtruediv(self):
+        """Works as of micropython ea7031f"""
+        res = "foo" / Path("bar")
+        self.assertTrue(res == Path("foo/bar"))
+
+    def test_rtruediv_inplace(self):
+        """Works as of micropython ea7031f"""
+        res = "foo"
+        res /= Path("bar")
+        self.assertTrue(res == Path("foo/bar"))
diff --git a/python-stdlib/pickle/manifest.py b/python-stdlib/pickle/manifest.py
new file mode 100644
index 000000000..412373a33
--- /dev/null
+++ b/python-stdlib/pickle/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.0")
+
+module("pickle.py")
diff --git a/python-stdlib/pickle/pickle.py b/python-stdlib/pickle/pickle.py
new file mode 100644
index 000000000..c4e113756
--- /dev/null
+++ b/python-stdlib/pickle/pickle.py
@@ -0,0 +1,26 @@
+HIGHEST_PROTOCOL = 0
+
+
+def dump(obj, f, proto=0):
+    f.write(repr(obj))
+
+
+def dumps(obj, proto=0):
+    return repr(obj).encode()
+
+
+def load(f):
+    s = f.read()
+    return loads(s)
+
+
+def loads(s):
+    d = {}
+    s = s.decode()
+    if "(" in s:
+        qualname = s.split("(", 1)[0]
+        if "." in qualname:
+            pkg = qualname.rsplit(".", 1)[0]
+            mod = __import__(pkg)
+            d[pkg] = mod
+    return eval(s, d)
diff --git a/python-stdlib/pickle/test_pickle.py b/python-stdlib/pickle/test_pickle.py
new file mode 100644
index 000000000..32fb7cc7b
--- /dev/null
+++ b/python-stdlib/pickle/test_pickle.py
@@ -0,0 +1,25 @@
+import pickle
+import sys
+import io
+
+
+def roundtrip(val):
+    t = pickle.dumps(val)
+    assert isinstance(t, bytes)
+    t = pickle.loads(t)
+    assert t == val
+
+
+roundtrip(1)
+roundtrip(1.0)
+roundtrip("str")
+roundtrip(b"bytes")
+roundtrip((1,))
+roundtrip([1, 2])
+roundtrip({1: 2, 3: 4})
+
+try:
+    pickle.loads(b"1; import micropython")
+    assert 0, "SyntaxError expected"
+except SyntaxError:
+    pass
diff --git a/python-stdlib/pkg_resources/manifest.py b/python-stdlib/pkg_resources/manifest.py
new file mode 100644
index 000000000..ba316c9c1
--- /dev/null
+++ b/python-stdlib/pkg_resources/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.2.1")
+
+module("pkg_resources.py")
diff --git a/python-stdlib/pkg_resources/pkg_resources.py b/python-stdlib/pkg_resources/pkg_resources.py
new file mode 100644
index 000000000..d69cb0577
--- /dev/null
+++ b/python-stdlib/pkg_resources/pkg_resources.py
@@ -0,0 +1,28 @@
+import io
+
+c = {}
+
+
+def resource_stream(package, resource):
+    if package not in c:
+        try:
+            if package:
+                p = __import__(package + ".R", None, None, True)
+            else:
+                p = __import__("R")
+            c[package] = p.R
+        except ImportError:
+            if package:
+                p = __import__(package)
+                d = p.__path__
+            else:
+                d = "."
+            #            if d[0] != "/":
+            #                import os
+            #                d = os.getcwd() + "/" + d
+            c[package] = d + "/"
+
+    p = c[package]
+    if isinstance(p, dict):
+        return io.BytesIO(p[resource])
+    return open(p + resource, "rb")
diff --git a/python-stdlib/pkgutil/manifest.py b/python-stdlib/pkgutil/manifest.py
new file mode 100644
index 000000000..5e5f13b27
--- /dev/null
+++ b/python-stdlib/pkgutil/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.1.1")
+
+require("pkg_resources")
+
+module("pkgutil.py")
diff --git a/pkgutil/pkgutil.py b/python-stdlib/pkgutil/pkgutil.py
similarity index 99%
rename from pkgutil/pkgutil.py
rename to python-stdlib/pkgutil/pkgutil.py
index 793547793..a93e83197 100644
--- a/pkgutil/pkgutil.py
+++ b/python-stdlib/pkgutil/pkgutil.py
@@ -1,5 +1,6 @@
 import pkg_resources
 
+
 def get_data(package, resource):
     f = pkg_resources.resource_stream(package, resource)
     try:
diff --git a/python-stdlib/pprint/manifest.py b/python-stdlib/pprint/manifest.py
new file mode 100644
index 000000000..9c0ebe9ed
--- /dev/null
+++ b/python-stdlib/pprint/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.0.4")
+
+module("pprint.py")
diff --git a/python-stdlib/pprint/pprint.py b/python-stdlib/pprint/pprint.py
new file mode 100644
index 000000000..3c140cadc
--- /dev/null
+++ b/python-stdlib/pprint/pprint.py
@@ -0,0 +1,6 @@
+def pformat(obj):
+    return repr(obj)
+
+
+def pprint(obj):
+    print(repr(obj))
diff --git a/python-stdlib/quopri/manifest.py b/python-stdlib/quopri/manifest.py
new file mode 100644
index 000000000..b7336972f
--- /dev/null
+++ b/python-stdlib/quopri/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.5.1")
+
+module("quopri.py")
diff --git a/python-stdlib/quopri/quopri.py b/python-stdlib/quopri/quopri.py
new file mode 100644
index 000000000..3ba07a22b
--- /dev/null
+++ b/python-stdlib/quopri/quopri.py
@@ -0,0 +1,257 @@
+#! /usr/bin/env python3
+
+"""Conversions to/from quoted-printable transport encoding as per RFC 1521."""
+
+# (Dec 1991 version).
+
+__all__ = ["encode", "decode", "encodestring", "decodestring"]
+
+ESCAPE = b"="
+MAXLINESIZE = 76
+HEX = b"0123456789ABCDEF"
+EMPTYSTRING = b""
+
+try:
+    from binascii import a2b_qp, b2a_qp
+except ImportError:
+    a2b_qp = None
+    b2a_qp = None
+
+
+def needsquoting(c, quotetabs, header):
+    """Decide whether a particular byte ordinal needs to be quoted.
+
+    The 'quotetabs' flag indicates whether embedded tabs and spaces should be
+    quoted.  Note that line-ending tabs and spaces are always encoded, as per
+    RFC 1521.
+    """
+    assert isinstance(c, bytes)
+    if c in b" \t":
+        return quotetabs
+    # if header, we have to escape _ because _ is used to escape space
+    if c == b"_":
+        return header
+    return c == ESCAPE or not (b" " <= c <= b"~")
+
+
+def quote(c):
+    """Quote a single character."""
+    assert isinstance(c, bytes) and len(c) == 1
+    c = ord(c)
+    return ESCAPE + bytes((HEX[c // 16], HEX[c % 16]))
+
+
+def encode(input, output, quotetabs, header=False):
+    """Read 'input', apply quoted-printable encoding, and write to 'output'.
+
+    'input' and 'output' are files with readline() and write() methods.
+    The 'quotetabs' flag indicates whether embedded tabs and spaces should be
+    quoted.  Note that line-ending tabs and spaces are always encoded, as per
+    RFC 1521.
+    The 'header' flag indicates whether we are encoding spaces as _ as per
+    RFC 1522.
+    """
+
+    if b2a_qp is not None:
+        data = input.read()
+        odata = b2a_qp(data, quotetabs=quotetabs, header=header)
+        output.write(odata)
+        return
+
+    def write(s, output=output, lineEnd=b"\n"):
+        # RFC 1521 requires that the line ending in a space or tab must have
+        # that trailing character encoded.
+        if s and s[-1:] in b" \t":
+            output.write(s[:-1] + quote(s[-1:]) + lineEnd)
+        elif s == b".":
+            output.write(quote(s) + lineEnd)
+        else:
+            output.write(s + lineEnd)
+
+    prevline = None
+    while 1:
+        line = input.readline()
+        if not line:
+            break
+        outline = []
+        # Strip off any readline induced trailing newline
+        stripped = b""
+        if line[-1:] == b"\n":
+            line = line[:-1]
+            stripped = b"\n"
+        # Calculate the un-length-limited encoded line
+        for c in line:
+            c = bytes((c,))
+            if needsquoting(c, quotetabs, header):
+                c = quote(c)
+            if header and c == b" ":
+                outline.append(b"_")
+            else:
+                outline.append(c)
+        # First, write out the previous line
+        if prevline is not None:
+            write(prevline)
+        # Now see if we need any soft line breaks because of RFC-imposed
+        # length limitations.  Then do the thisline->prevline dance.
+        thisline = EMPTYSTRING.join(outline)
+        while len(thisline) > MAXLINESIZE:
+            # Don't forget to include the soft line break `=' sign in the
+            # length calculation!
+            write(thisline[: MAXLINESIZE - 1], lineEnd=b"=\n")
+            thisline = thisline[MAXLINESIZE - 1 :]
+        # Write out the current line
+        prevline = thisline
+    # Write out the last line, without a trailing newline
+    if prevline is not None:
+        write(prevline, lineEnd=stripped)
+
+
+def encodestring(s, quotetabs=False, header=False):
+    if b2a_qp is not None:
+        return b2a_qp(s, quotetabs=quotetabs, header=header)
+    from io import BytesIO
+
+    infp = BytesIO(s)
+    outfp = BytesIO()
+    encode(infp, outfp, quotetabs, header)
+    return outfp.getvalue()
+
+
+def decode(input, output, header=False):
+    """Read 'input', apply quoted-printable decoding, and write to 'output'.
+    'input' and 'output' are files with readline() and write() methods.
+    If 'header' is true, decode underscore as space (per RFC 1522)."""
+
+    if a2b_qp is not None:
+        data = input.read()
+        odata = a2b_qp(data, header=header)
+        output.write(odata)
+        return
+
+    new = b""
+    while 1:
+        line = input.readline()
+        if not line:
+            break
+        i, n = 0, len(line)
+        if n > 0 and line[n - 1 : n] == b"\n":
+            partial = 0
+            n = n - 1
+            # Strip trailing whitespace
+            while n > 0 and line[n - 1 : n] in b" \t\r":
+                n = n - 1
+        else:
+            partial = 1
+        while i < n:
+            c = line[i : i + 1]
+            if c == b"_" and header:
+                new = new + b" "
+                i = i + 1
+            elif c != ESCAPE:
+                new = new + c
+                i = i + 1
+            elif i + 1 == n and not partial:
+                partial = 1
+                break
+            elif i + 1 < n and line[i + 1] == ESCAPE:
+                new = new + ESCAPE
+                i = i + 2
+            elif i + 2 < n and ishex(line[i + 1 : i + 2]) and ishex(line[i + 2 : i + 3]):
+                new = new + bytes((unhex(line[i + 1 : i + 3]),))
+                i = i + 3
+            else:  # Bad escape sequence -- leave it in
+                new = new + c
+                i = i + 1
+        if not partial:
+            output.write(new + b"\n")
+            new = b""
+    if new:
+        output.write(new)
+
+
+def decodestring(s, header=False):
+    if a2b_qp is not None:
+        return a2b_qp(s, header=header)
+    from io import BytesIO
+
+    infp = BytesIO(s)
+    outfp = BytesIO()
+    decode(infp, outfp, header=header)
+    return outfp.getvalue()
+
+
+# Other helper functions
+def ishex(c):
+    """Return true if the byte ordinal 'c' is a hexadecimal digit in ASCII."""
+    assert isinstance(c, bytes)
+    return b"0" <= c <= b"9" or b"a" <= c <= b"f" or b"A" <= c <= b"F"
+
+
+def unhex(s):
+    """Get the integer value of a hexadecimal number."""
+    bits = 0
+    for c in s:
+        c = bytes((c,))
+        if b"0" <= c <= b"9":
+            i = ord("0")
+        elif b"a" <= c <= b"f":
+            i = ord("a") - 10
+        elif b"A" <= c <= b"F":
+            i = ord(b"A") - 10
+        else:
+            assert False, "non-hex digit " + repr(c)
+        bits = bits * 16 + (ord(c) - i)
+    return bits
+
+
+def main():
+    import sys
+    import getopt
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "td")
+    except getopt.error as msg:
+        sys.stdout = sys.stderr
+        print(msg)
+        print("usage: quopri [-t | -d] [file] ...")
+        print("-t: quote tabs")
+        print("-d: decode; default encode")
+        sys.exit(2)
+    deco = 0
+    tabs = 0
+    for o, a in opts:
+        if o == "-t":
+            tabs = 1
+        if o == "-d":
+            deco = 1
+    if tabs and deco:
+        sys.stdout = sys.stderr
+        print("-t and -d are mutually exclusive")
+        sys.exit(2)
+    if not args:
+        args = ["-"]
+    sts = 0
+    for file in args:
+        if file == "-":
+            fp = sys.stdin.buffer
+        else:
+            try:
+                fp = open(file, "rb")
+            except IOError as msg:
+                sys.stderr.write("%s: can't open (%s)\n" % (file, msg))
+                sts = 1
+                continue
+        try:
+            if deco:
+                decode(fp, sys.stdout.buffer)
+            else:
+                encode(fp, sys.stdout.buffer, tabs)
+        finally:
+            if file != "-":
+                fp.close()
+    if sts:
+        sys.exit(sts)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/python-stdlib/quopri/test_quopri.py b/python-stdlib/quopri/test_quopri.py
new file mode 100644
index 000000000..b87e54842
--- /dev/null
+++ b/python-stdlib/quopri/test_quopri.py
@@ -0,0 +1,223 @@
+import unittest
+
+import sys, os, io
+import quopri
+
+
+ENCSAMPLE = b"""\
+Here's a bunch of special=20
+
+=A1=A2=A3=A4=A5=A6=A7=A8=A9
+=AA=AB=AC=AD=AE=AF=B0=B1=B2=B3
+=B4=B5=B6=B7=B8=B9=BA=BB=BC=BD=BE
+=BF=C0=C1=C2=C3=C4=C5=C6
+=C7=C8=C9=CA=CB=CC=CD=CE=CF
+=D0=D1=D2=D3=D4=D5=D6=D7
+=D8=D9=DA=DB=DC=DD=DE=DF
+=E0=E1=E2=E3=E4=E5=E6=E7
+=E8=E9=EA=EB=EC=ED=EE=EF
+=F0=F1=F2=F3=F4=F5=F6=F7
+=F8=F9=FA=FB=FC=FD=FE=FF
+
+characters... have fun!
+"""
+
+# First line ends with a space
+DECSAMPLE = (
+    b"Here's a bunch of special \n"
+    + b"""\
+
+\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9
+\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3
+\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe
+\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6
+\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf
+\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7
+\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf
+\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7
+\xe8\xe9\xea\xeb\xec\xed\xee\xef
+\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7
+\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff
+
+characters... have fun!
+"""
+)
+
+
+def withpythonimplementation(testfunc):
+    def newtest(self):
+        # Test default implementation
+        testfunc(self)
+        # Test Python implementation
+        if quopri.b2a_qp is not None or quopri.a2b_qp is not None:
+            oldencode = quopri.b2a_qp
+            olddecode = quopri.a2b_qp
+            try:
+                quopri.b2a_qp = None
+                quopri.a2b_qp = None
+                testfunc(self)
+            finally:
+                quopri.b2a_qp = oldencode
+                quopri.a2b_qp = olddecode
+
+    #    newtest.__name__ = testfunc.__name__
+    return newtest
+
+
+class QuopriTestCase(unittest.TestCase):
+    # Each entry is a tuple of (plaintext, encoded string).  These strings are
+    # used in the "quotetabs=0" tests.
+    STRINGS = (
+        # Some normal strings
+        (b"hello", b"hello"),
+        (
+            b"""hello
+        there
+        world""",
+            b"""hello
+        there
+        world""",
+        ),
+        (
+            b"""hello
+        there
+        world
+""",
+            b"""hello
+        there
+        world
+""",
+        ),
+        (b"\201\202\203", b"=81=82=83"),
+        # Add some trailing MUST QUOTE strings
+        (b"hello ", b"hello=20"),
+        (b"hello\t", b"hello=09"),
+        # Some long lines.  First, a single line of 108 characters
+        (
+            b"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\xd8\xd9\xda\xdb\xdc\xdd\xde\xdfxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+            b"""xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=D8=D9=DA=DB=DC=DD=DE=DFx=
+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""",
+        ),
+        # A line of exactly 76 characters, no soft line break should be needed
+        (
+            b"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
+            b"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
+        ),
+        # A line of 77 characters, forcing a soft line break at position 75,
+        # and a second line of exactly 2 characters (because the soft line
+        # break `=' sign counts against the line length limit).
+        (
+            b"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
+            b"""zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=
+zz""",
+        ),
+        # A line of 151 characters, forcing a soft line break at position 75,
+        # with a second line of exactly 76 characters and no trailing =
+        (
+            b"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
+            b"""zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=
+zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz""",
+        ),
+        # A string containing a hard line break, but which the first line is
+        # 151 characters and the second line is exactly 76 characters.  This
+        # should leave us with three lines, the first which has a soft line
+        # break, and which the second and third do not.
+        (
+            b"""yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz""",
+            b"""yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy=
+yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz""",
+        ),
+        # Now some really complex stuff ;)
+        (DECSAMPLE, ENCSAMPLE),
+    )
+
+    # These are used in the "quotetabs=1" tests.
+    ESTRINGS = (
+        (b"hello world", b"hello=20world"),
+        (b"hello\tworld", b"hello=09world"),
+    )
+
+    # These are used in the "header=1" tests.
+    HSTRINGS = (
+        (b"hello world", b"hello_world"),
+        (b"hello_world", b"hello=5Fworld"),
+    )
+
+    @withpythonimplementation
+    def test_encodestring(self):
+        for p, e in self.STRINGS:
+            self.assertEqual(quopri.encodestring(p), e)
+
+    @withpythonimplementation
+    def test_decodestring(self):
+        for p, e in self.STRINGS:
+            self.assertEqual(quopri.decodestring(e), p)
+
+    @withpythonimplementation
+    def test_idempotent_string(self):
+        for p, e in self.STRINGS:
+            self.assertEqual(quopri.decodestring(quopri.encodestring(e)), e)
+
+    @withpythonimplementation
+    def test_encode(self):
+        for p, e in self.STRINGS:
+            infp = io.BytesIO(p)
+            outfp = io.BytesIO()
+            quopri.encode(infp, outfp, quotetabs=False)
+            self.assertEqual(outfp.getvalue(), e)
+
+    @withpythonimplementation
+    def test_decode(self):
+        for p, e in self.STRINGS:
+            infp = io.BytesIO(e)
+            outfp = io.BytesIO()
+            quopri.decode(infp, outfp)
+            self.assertEqual(outfp.getvalue(), p)
+
+    @withpythonimplementation
+    def test_embedded_ws(self):
+        for p, e in self.ESTRINGS:
+            self.assertEqual(quopri.encodestring(p, quotetabs=True), e)
+            self.assertEqual(quopri.decodestring(e), p)
+
+    @withpythonimplementation
+    def test_encode_header(self):
+        for p, e in self.HSTRINGS:
+            self.assertEqual(quopri.encodestring(p, header=True), e)
+
+    @withpythonimplementation
+    def test_decode_header(self):
+        for p, e in self.HSTRINGS:
+            self.assertEqual(quopri.decodestring(e, header=True), p)
+
+    @unittest.skip("requires subprocess")
+    def test_scriptencode(self):
+        (p, e) = self.STRINGS[-1]
+        process = subprocess.Popen(
+            [sys.executable, "-mquopri"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
+        )
+        self.addCleanup(process.stdout.close)
+        cout, cerr = process.communicate(p)
+        # On Windows, Python will output the result to stdout using
+        # CRLF, as the mode of stdout is text mode. To compare this
+        # with the expected result, we need to do a line-by-line comparison.
+        cout = cout.decode("latin-1").splitlines()
+        e = e.decode("latin-1").splitlines()
+        assert len(cout) == len(e)
+        for i in range(len(cout)):
+            self.assertEqual(cout[i], e[i])
+        self.assertEqual(cout, e)
+
+    @unittest.skip("requires subprocess")
+    def test_scriptdecode(self):
+        (p, e) = self.STRINGS[-1]
+        process = subprocess.Popen(
+            [sys.executable, "-mquopri", "-d"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
+        )
+        self.addCleanup(process.stdout.close)
+        cout, cerr = process.communicate(e)
+        cout = cout.decode("latin-1")
+        p = p.decode("latin-1")
+        self.assertEqual(cout.splitlines(), p.splitlines())
diff --git a/python-stdlib/random/manifest.py b/python-stdlib/random/manifest.py
new file mode 100644
index 000000000..e09c8b0f7
--- /dev/null
+++ b/python-stdlib/random/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.2.0")
+
+module("random.py")
diff --git a/python-stdlib/random/random.py b/python-stdlib/random/random.py
new file mode 100644
index 000000000..f1d307451
--- /dev/null
+++ b/python-stdlib/random/random.py
@@ -0,0 +1,44 @@
+from urandom import *
+
+_getrandbits32 = getrandbits
+
+
+def getrandbits(bits: int) -> int:
+    n = bits // 32
+    d = 0
+    for i in range(n):
+        d |= _getrandbits32(32) << (i * 32)
+
+    r = bits % 32
+    if r >= 1:
+        d |= _getrandbits32(r) << (n * 32)
+
+    return d
+
+
+def randrange(start, stop=None):
+    if stop is None:
+        stop = start
+        start = 0
+    upper = stop - start
+    bits = 0
+    pwr2 = 1
+    while upper > pwr2:
+        pwr2 <<= 1
+        bits += 1
+    while True:
+        r = getrandbits(bits)
+        if r < upper:
+            break
+    return r + start
+
+
+def randint(start, stop):
+    return randrange(start, stop + 1)
+
+
+def shuffle(seq):
+    l = len(seq)
+    for i in range(l):
+        j = randrange(l)
+        seq[i], seq[j] = seq[j], seq[i]
diff --git a/python-stdlib/random/test_randrange.py b/python-stdlib/random/test_randrange.py
new file mode 100644
index 000000000..fdd35a4b5
--- /dev/null
+++ b/python-stdlib/random/test_randrange.py
@@ -0,0 +1,14 @@
+from random import *
+
+
+UPPER = 100
+
+s = set()
+
+# This number of course depends on a particular PRNG and its default seed
+# as used by MicroPython.
+for c in range(496):
+    r = randrange(UPPER)
+    s.add(r)
+
+assert len(s) == UPPER
diff --git a/python-stdlib/shutil/manifest.py b/python-stdlib/shutil/manifest.py
new file mode 100644
index 000000000..966689e90
--- /dev/null
+++ b/python-stdlib/shutil/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.0.5")
+
+module("shutil.py")
diff --git a/python-stdlib/shutil/shutil.py b/python-stdlib/shutil/shutil.py
new file mode 100644
index 000000000..9e72c8ea6
--- /dev/null
+++ b/python-stdlib/shutil/shutil.py
@@ -0,0 +1,48 @@
+# Reimplement, because CPython3.3 impl is rather bloated
+import os
+from collections import namedtuple
+
+_ntuple_diskusage = namedtuple("usage", ("total", "used", "free"))
+
+
+def rmtree(d):
+    if not d:
+        raise ValueError
+
+    for name, type, *_ in os.ilistdir(d):
+        path = d + "/" + name
+        if type & 0x4000:  # dir
+            rmtree(path)
+        else:  # file
+            os.unlink(path)
+    os.rmdir(d)
+
+
+def copyfileobj(src, dest, length=512):
+    if hasattr(src, "readinto"):
+        buf = bytearray(length)
+        while True:
+            sz = src.readinto(buf)
+            if not sz:
+                break
+            if sz == length:
+                dest.write(buf)
+            else:
+                b = memoryview(buf)[:sz]
+                dest.write(b)
+    else:
+        while True:
+            buf = src.read(length)
+            if not buf:
+                break
+            dest.write(buf)
+
+
+def disk_usage(path):
+    bit_tuple = os.statvfs(path)
+    blksize = bit_tuple[0]  # system block size
+    total = bit_tuple[2] * blksize
+    free = bit_tuple[3] * blksize
+    used = total - free
+
+    return _ntuple_diskusage(total, used, free)
diff --git a/python-stdlib/shutil/test_shutil.py b/python-stdlib/shutil/test_shutil.py
new file mode 100644
index 000000000..d8b8632c5
--- /dev/null
+++ b/python-stdlib/shutil/test_shutil.py
@@ -0,0 +1,56 @@
+"""
+Don't use ``tempfile`` in these tests, as ``tempfile`` relies on ``shutil``.
+"""
+
+import os
+import shutil
+import unittest
+
+
+class TestRmtree(unittest.TestCase):
+    def test_dir_dne(self):
+        with self.assertRaises(OSError):
+            os.stat("foo")
+
+        with self.assertRaises(OSError):
+            shutil.rmtree("foo")
+
+    def test_file(self):
+        fn = "foo"
+        with open(fn, "w"):
+            pass
+
+        with self.assertRaises(OSError):
+            shutil.rmtree(fn)
+
+        os.remove(fn)
+
+    def test_empty_dir(self):
+        with self.assertRaises(OSError):
+            # If this triggers, a previous test didn't clean up.
+            # bit of a chicken/egg situation with ``tempfile``
+            os.stat("foo")
+
+        os.mkdir("foo")
+        shutil.rmtree("foo")
+
+        with self.assertRaises(OSError):
+            os.stat("foo")
+
+    def test_dir(self):
+        with self.assertRaises(OSError):
+            # If this triggers, a previous test didn't clean up.
+            # bit of a chicken/egg situation with ``tempfile``
+            os.stat("foo")
+
+        os.mkdir("foo")
+        os.mkdir("foo/bar")
+        with open("foo/bar/baz1.txt", "w"):
+            pass
+        with open("foo/bar/baz2.txt", "w"):
+            pass
+
+        shutil.rmtree("foo")
+
+        with self.assertRaises(OSError):
+            os.stat("foo")
diff --git a/python-stdlib/ssl/manifest.py b/python-stdlib/ssl/manifest.py
new file mode 100644
index 000000000..a99523071
--- /dev/null
+++ b/python-stdlib/ssl/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.2.1")
+
+module("ssl.py", opt=3)
diff --git a/python-stdlib/ssl/ssl.py b/python-stdlib/ssl/ssl.py
new file mode 100644
index 000000000..c61904be7
--- /dev/null
+++ b/python-stdlib/ssl/ssl.py
@@ -0,0 +1,65 @@
+import tls
+from tls import *
+
+
+class SSLContext:
+    def __init__(self, *args):
+        self._context = tls.SSLContext(*args)
+        self._context.verify_mode = CERT_NONE
+
+    @property
+    def verify_mode(self):
+        return self._context.verify_mode
+
+    @verify_mode.setter
+    def verify_mode(self, val):
+        self._context.verify_mode = val
+
+    def load_cert_chain(self, certfile, keyfile):
+        if isinstance(certfile, str):
+            with open(certfile, "rb") as f:
+                certfile = f.read()
+        if isinstance(keyfile, str):
+            with open(keyfile, "rb") as f:
+                keyfile = f.read()
+        self._context.load_cert_chain(certfile, keyfile)
+
+    def load_verify_locations(self, cafile=None, cadata=None):
+        if cafile:
+            with open(cafile, "rb") as f:
+                cadata = f.read()
+        self._context.load_verify_locations(cadata)
+
+    def wrap_socket(
+        self, sock, server_side=False, do_handshake_on_connect=True, server_hostname=None
+    ):
+        return self._context.wrap_socket(
+            sock,
+            server_side=server_side,
+            do_handshake_on_connect=do_handshake_on_connect,
+            server_hostname=server_hostname,
+        )
+
+
+def wrap_socket(
+    sock,
+    server_side=False,
+    key=None,
+    cert=None,
+    cert_reqs=CERT_NONE,
+    cadata=None,
+    server_hostname=None,
+    do_handshake=True,
+):
+    con = SSLContext(PROTOCOL_TLS_SERVER if server_side else PROTOCOL_TLS_CLIENT)
+    if cert or key:
+        con.load_cert_chain(cert, key)
+    if cadata:
+        con.load_verify_locations(cadata=cadata)
+    con.verify_mode = cert_reqs
+    return con.wrap_socket(
+        sock,
+        server_side=server_side,
+        do_handshake_on_connect=do_handshake,
+        server_hostname=server_hostname,
+    )
diff --git a/python-stdlib/stat/manifest.py b/python-stdlib/stat/manifest.py
new file mode 100644
index 000000000..a4a0b811c
--- /dev/null
+++ b/python-stdlib/stat/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.5.1")
+
+module("stat.py")
diff --git a/python-stdlib/stat/stat.py b/python-stdlib/stat/stat.py
new file mode 100644
index 000000000..f5ebea8f4
--- /dev/null
+++ b/python-stdlib/stat/stat.py
@@ -0,0 +1,154 @@
+"""Constants/functions for interpreting results of os.stat() and os.lstat().
+
+Suggested usage: from stat import *
+"""
+
+# Indices for stat struct members in the tuple returned by os.stat()
+
+ST_MODE = 0
+ST_INO = 1
+ST_DEV = 2
+ST_NLINK = 3
+ST_UID = 4
+ST_GID = 5
+ST_SIZE = 6
+ST_ATIME = 7
+ST_MTIME = 8
+ST_CTIME = 9
+
+# Extract bits from the mode
+
+
+def S_IMODE(mode):
+    """Return the portion of the file's mode that can be set by
+    os.chmod().
+    """
+    return mode & 0o7777
+
+
+def S_IFMT(mode):
+    """Return the portion of the file's mode that describes the
+    file type.
+    """
+    return mode & 0o170000
+
+
+# Constants used as S_IFMT() for various file types
+# (not all are implemented on all systems)
+
+S_IFDIR = 0o040000  # directory
+S_IFCHR = 0o020000  # character device
+S_IFBLK = 0o060000  # block device
+S_IFREG = 0o100000  # regular file
+S_IFIFO = 0o010000  # fifo (named pipe)
+S_IFLNK = 0o120000  # symbolic link
+S_IFSOCK = 0o140000  # socket file
+
+# Functions to test for each file type
+
+
+def S_ISDIR(mode):
+    """Return True if mode is from a directory."""
+    return S_IFMT(mode) == S_IFDIR
+
+
+def S_ISCHR(mode):
+    """Return True if mode is from a character special device file."""
+    return S_IFMT(mode) == S_IFCHR
+
+
+def S_ISBLK(mode):
+    """Return True if mode is from a block special device file."""
+    return S_IFMT(mode) == S_IFBLK
+
+
+def S_ISREG(mode):
+    """Return True if mode is from a regular file."""
+    return S_IFMT(mode) == S_IFREG
+
+
+def S_ISFIFO(mode):
+    """Return True if mode is from a FIFO (named pipe)."""
+    return S_IFMT(mode) == S_IFIFO
+
+
+def S_ISLNK(mode):
+    """Return True if mode is from a symbolic link."""
+    return S_IFMT(mode) == S_IFLNK
+
+
+def S_ISSOCK(mode):
+    """Return True if mode is from a socket."""
+    return S_IFMT(mode) == S_IFSOCK
+
+
+# Names for permission bits
+
+S_ISUID = 0o4000  # set UID bit
+S_ISGID = 0o2000  # set GID bit
+S_ENFMT = S_ISGID  # file locking enforcement
+S_ISVTX = 0o1000  # sticky bit
+S_IREAD = 0o0400  # Unix V7 synonym for S_IRUSR
+S_IWRITE = 0o0200  # Unix V7 synonym for S_IWUSR
+S_IEXEC = 0o0100  # Unix V7 synonym for S_IXUSR
+S_IRWXU = 0o0700  # mask for owner permissions
+S_IRUSR = 0o0400  # read by owner
+S_IWUSR = 0o0200  # write by owner
+S_IXUSR = 0o0100  # execute by owner
+S_IRWXG = 0o0070  # mask for group permissions
+S_IRGRP = 0o0040  # read by group
+S_IWGRP = 0o0020  # write by group
+S_IXGRP = 0o0010  # execute by group
+S_IRWXO = 0o0007  # mask for others (not in group) permissions
+S_IROTH = 0o0004  # read by others
+S_IWOTH = 0o0002  # write by others
+S_IXOTH = 0o0001  # execute by others
+
+# Names for file flags
+
+UF_NODUMP = 0x00000001  # do not dump file
+UF_IMMUTABLE = 0x00000002  # file may not be changed
+UF_APPEND = 0x00000004  # file may only be appended to
+UF_OPAQUE = 0x00000008  # directory is opaque when viewed through a union stack
+UF_NOUNLINK = 0x00000010  # file may not be renamed or deleted
+UF_COMPRESSED = 0x00000020  # OS X: file is hfs-compressed
+UF_HIDDEN = 0x00008000  # OS X: file should not be displayed
+SF_ARCHIVED = 0x00010000  # file may be archived
+SF_IMMUTABLE = 0x00020000  # file may not be changed
+SF_APPEND = 0x00040000  # file may only be appended to
+SF_NOUNLINK = 0x00100000  # file may not be renamed or deleted
+SF_SNAPSHOT = 0x00200000  # file is a snapshot file
+
+
+_filemode_table = (
+    (
+        (S_IFLNK, "l"),
+        (S_IFREG, "-"),
+        (S_IFBLK, "b"),
+        (S_IFDIR, "d"),
+        (S_IFCHR, "c"),
+        (S_IFIFO, "p"),
+    ),
+    ((S_IRUSR, "r"),),
+    ((S_IWUSR, "w"),),
+    ((S_IXUSR | S_ISUID, "s"), (S_ISUID, "S"), (S_IXUSR, "x")),
+    ((S_IRGRP, "r"),),
+    ((S_IWGRP, "w"),),
+    ((S_IXGRP | S_ISGID, "s"), (S_ISGID, "S"), (S_IXGRP, "x")),
+    ((S_IROTH, "r"),),
+    ((S_IWOTH, "w"),),
+    ((S_IXOTH | S_ISVTX, "t"), (S_ISVTX, "T"), (S_IXOTH, "x")),
+)
+
+
+def filemode(mode):
+    """Convert a file's mode to a string of the form '-rwxrwxrwx'."""
+    perm = []
+    for table in _filemode_table:
+        for bit, char in table:
+            if mode & bit == bit:
+                perm.append(char)
+                break
+        else:
+            perm.append("-")
+    return "".join(perm)
diff --git a/python-stdlib/string/manifest.py b/python-stdlib/string/manifest.py
new file mode 100644
index 000000000..a6b552be2
--- /dev/null
+++ b/python-stdlib/string/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.1")
+
+module("string.py")
diff --git a/python-stdlib/string/string.py b/python-stdlib/string/string.py
new file mode 100644
index 000000000..5937ace51
--- /dev/null
+++ b/python-stdlib/string/string.py
@@ -0,0 +1,27 @@
+# Some strings for ctype-style character classification
+whitespace = " \t\n\r\v\f"
+ascii_lowercase = "abcdefghijklmnopqrstuvwxyz"
+ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ascii_letters = ascii_lowercase + ascii_uppercase
+digits = "0123456789"
+hexdigits = digits + "abcdef" + "ABCDEF"
+octdigits = "01234567"
+punctuation = """!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
+printable = digits + ascii_letters + punctuation + whitespace
+
+
+def translate(s, map):
+    import io
+
+    sb = io.StringIO()
+    for c in s:
+        v = ord(c)
+        if v in map:
+            v = map[v]
+            if isinstance(v, int):
+                sb.write(chr(v))
+            elif v is not None:
+                sb.write(v)
+        else:
+            sb.write(c)
+    return sb.getvalue()
diff --git a/string/test_translate.py b/python-stdlib/string/test_translate.py
similarity index 100%
rename from string/test_translate.py
rename to python-stdlib/string/test_translate.py
diff --git a/python-stdlib/struct/manifest.py b/python-stdlib/struct/manifest.py
new file mode 100644
index 000000000..4535d780e
--- /dev/null
+++ b/python-stdlib/struct/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.1")
+
+module("struct.py")
diff --git a/struct/struct.py b/python-stdlib/struct/struct.py
similarity index 100%
rename from struct/struct.py
rename to python-stdlib/struct/struct.py
index 74ed9038e..f228c73a3 100644
--- a/struct/struct.py
+++ b/python-stdlib/struct/struct.py
@@ -1,7 +1,7 @@
 from ustruct import *
 
-class Struct:
 
+class Struct:
     def __init__(self, format):
         self.format = format
         self.size = calcsize(format)
diff --git a/python-stdlib/tarfile-write/example-append.py b/python-stdlib/tarfile-write/example-append.py
new file mode 100644
index 000000000..f496eb3aa
--- /dev/null
+++ b/python-stdlib/tarfile-write/example-append.py
@@ -0,0 +1,15 @@
+""" tar append writes additional files to the end of an existing tar file."""
+import os
+import sys
+import tarfile
+
+if len(sys.argv) < 2:
+    raise ValueError("Usage: %s appendfile.tar newinputfile1 ..." % sys.argv[0])
+
+tarfile = sys.argv[1]
+if not tarfile.endswith(".tar"):
+    raise ValueError("Filename %s does not end with .tar" % tarfile)
+
+with tarfile.TarFile(sys.argv[1], "a") as t:
+    for filename in sys.argv[2:]:
+        t.add(filename)
diff --git a/python-stdlib/tarfile-write/example-create.py b/python-stdlib/tarfile-write/example-create.py
new file mode 100644
index 000000000..ee6ec6255
--- /dev/null
+++ b/python-stdlib/tarfile-write/example-create.py
@@ -0,0 +1,14 @@
+""" tar create writes a new tar file containing the specified files."""
+import sys
+import tarfile
+
+if len(sys.argv) < 2:
+    raise ValueError("Usage: %s outputfile.tar inputfile1 ..." % sys.argv[0])
+
+tarfile = sys.argv[1]
+if not tarfile.endswith(".tar"):
+    raise ValueError("Filename %s does not end with .tar" % tarfile)
+
+with tarfile.TarFile(sys.argv[1], "w") as t:
+    for filename in sys.argv[2:]:
+        t.add(filename)
diff --git a/python-stdlib/tarfile-write/manifest.py b/python-stdlib/tarfile-write/manifest.py
new file mode 100644
index 000000000..bc4f37741
--- /dev/null
+++ b/python-stdlib/tarfile-write/manifest.py
@@ -0,0 +1,4 @@
+metadata(description="Adds write (create/append) support to tarfile.", version="0.1.2")
+
+require("tarfile")
+package("tarfile")
diff --git a/python-stdlib/tarfile-write/tarfile/write.py b/python-stdlib/tarfile-write/tarfile/write.py
new file mode 100644
index 000000000..527b3317b
--- /dev/null
+++ b/python-stdlib/tarfile-write/tarfile/write.py
@@ -0,0 +1,121 @@
+"""Additions to the TarFile class to support creating and appending tar files.
+
+The methods defined below in are injected into the TarFile class in the
+tarfile package.
+"""
+
+import uctypes
+import os
+
+# Extended subset of tar header fields including the ones we'll write.
+# http://www.gnu.org/software/tar/manual/html_node/Standard.html
+_TAR_HEADER = {
+    "name": (uctypes.ARRAY | 0, uctypes.UINT8 | 100),
+    "mode": (uctypes.ARRAY | 100, uctypes.UINT8 | 8),
+    "uid": (uctypes.ARRAY | 108, uctypes.UINT8 | 8),
+    "gid": (uctypes.ARRAY | 116, uctypes.UINT8 | 8),
+    "size": (uctypes.ARRAY | 124, uctypes.UINT8 | 12),
+    "mtime": (uctypes.ARRAY | 136, uctypes.UINT8 | 12),
+    "chksum": (uctypes.ARRAY | 148, uctypes.UINT8 | 8),
+    "typeflag": (uctypes.ARRAY | 156, uctypes.UINT8 | 1),
+}
+
+
+_NUL = const(b"\0")  # the null character
+_BLOCKSIZE = const(512)  # length of processing blocks
+_RECORDSIZE = const(_BLOCKSIZE * 20)  # length of records
+
+
+def _open_write(self, name, mode, fileobj):
+    if mode == "w":
+        if not fileobj:
+            self.f = open(name, "wb")
+        else:
+            self.f = fileobj
+    elif mode == "a":
+        if not fileobj:
+            self.f = open(name, "r+b")
+        else:
+            self.f = fileobj
+        # Read through the existing file.
+        while self.next():
+            pass
+        # Position at start of end block.
+        self.f.seek(self.offset)
+    else:
+        raise ValueError("mode " + mode + " not supported.")
+
+
+def _close_write(self):
+    # Must be called to complete writing a tar file.
+    if self.mode == "w":
+        self.f.write(_NUL * (_BLOCKSIZE * 2))
+        self.offset += _BLOCKSIZE * 2
+        remainder = self.offset % _RECORDSIZE
+        if remainder:
+            self.f.write(_NUL * (_RECORDSIZE - remainder))
+
+
+def addfile(self, tarinfo, fileobj=None):
+    # Write the header: 100 bytes of name, 8 bytes of mode in octal...
+    buf = bytearray(_BLOCKSIZE)
+    name = tarinfo.name
+    size = tarinfo.size
+    if tarinfo.isdir():
+        size = 0
+        if not name.endswith("/"):
+            name += "/"
+    hdr = uctypes.struct(uctypes.addressof(buf), _TAR_HEADER, uctypes.LITTLE_ENDIAN)
+    hdr.name[:] = name.encode("utf-8")[:100]
+    hdr.mode[:] = b"%07o\0" % ((0o755 if tarinfo.isdir() else 0o644) & 0o7777)
+    hdr.uid[:] = b"%07o\0" % tarinfo.uid
+    hdr.gid[:] = b"%07o\0" % tarinfo.gid
+    hdr.size[:] = b"%011o\0" % size
+    hdr.mtime[:] = b"%011o\0" % tarinfo.mtime
+    hdr.typeflag[:] = b"5" if tarinfo.isdir() else b"0"
+    # Checksum is calculated with checksum field all blanks.
+    hdr.chksum[:] = b"        "
+    # Calculate and insert the actual checksum.
+    chksum = sum(buf)
+    hdr.chksum[:] = b"%06o\0 " % chksum
+    # Emit the header.
+    self.f.write(buf)
+    self.offset += len(buf)
+
+    # Copy the file contents, if any.
+    if fileobj:
+        n_bytes = self.f.write(fileobj.read())
+        self.offset += n_bytes
+        remains = -n_bytes & (_BLOCKSIZE - 1)  # == 0b111111111
+        if remains:
+            buf = bytearray(remains)
+            self.f.write(buf)
+            self.offset += len(buf)
+
+
+def add(self, name, recursive=True):
+    from . import TarInfo
+
+    try:
+        stat = os.stat(name)
+        res_name = (name + '/') if (stat[0] & 0xf000) == 0x4000 else name
+        tarinfo = TarInfo(res_name)
+        tarinfo.mode = stat[0]
+        tarinfo.uid = stat[4]
+        tarinfo.gid = stat[5]
+        tarinfo.size = stat[6]
+        tarinfo.mtime = stat[8]
+    except OSError:
+        print("Cannot stat", name, " - skipping.")
+        return
+    if not (tarinfo.isdir() or tarinfo.isreg()):
+        # We only accept directories or regular files.
+        print(name, "is not a directory or regular file - skipping.")
+        return
+    if tarinfo.isdir():
+        self.addfile(tarinfo)
+        if recursive:
+            for f in os.ilistdir(name):
+                self.add(name + "/" + f[0], recursive)
+    else:  # type == REGTYPE
+        self.addfile(tarinfo, open(name, "rb"))
diff --git a/python-stdlib/tarfile/example-extract.py b/python-stdlib/tarfile/example-extract.py
new file mode 100644
index 000000000..94ce829ce
--- /dev/null
+++ b/python-stdlib/tarfile/example-extract.py
@@ -0,0 +1,16 @@
+import sys
+import os
+import tarfile
+
+if len(sys.argv) < 2:
+    raise ValueError("Usage: %s inputfile.tar" % sys.argv[0])
+
+t = tarfile.TarFile(sys.argv[1])
+for i in t:
+    print(i.name)
+    if i.type == tarfile.DIRTYPE:
+        os.mkdir(i.name)
+    else:
+        f = t.extractfile(i)
+        with open(i.name, "wb") as of:
+            of.write(f.read())
diff --git a/python-stdlib/tarfile/manifest.py b/python-stdlib/tarfile/manifest.py
new file mode 100644
index 000000000..9940bb051
--- /dev/null
+++ b/python-stdlib/tarfile/manifest.py
@@ -0,0 +1,5 @@
+metadata(description="Read-only implementation of Python's tarfile.", version="0.4.1")
+
+# Originally written by Paul Sokolovsky.
+
+package("tarfile")
diff --git a/python-stdlib/tarfile/tarfile/__init__.py b/python-stdlib/tarfile/tarfile/__init__.py
new file mode 100644
index 000000000..4bb95af30
--- /dev/null
+++ b/python-stdlib/tarfile/tarfile/__init__.py
@@ -0,0 +1,148 @@
+"""Subset of cpython tarfile class methods needed to decode tar files."""
+
+import uctypes
+
+# Minimal set of tar header fields for reading.
+# http://www.gnu.org/software/tar/manual/html_node/Standard.html
+# The "size" entry is 11 (not 12) to implicitly cut off the null terminator.
+_TAR_HEADER = {
+    "name": (uctypes.ARRAY | 0, uctypes.UINT8 | 100),
+    "size": (uctypes.ARRAY | 124, uctypes.UINT8 | 11),
+}
+
+DIRTYPE = const("dir")
+REGTYPE = const("file")
+
+# Constants for TarInfo.isdir, isreg.
+_S_IFMT = const(0o170000)
+_S_IFREG = const(0o100000)
+_S_IFDIR = const(0o040000)
+
+_BLOCKSIZE = const(512)  # length of processing blocks
+
+
+def _roundup(val, align):
+    return (val + align - 1) & ~(align - 1)
+
+
+class FileSection:
+    def __init__(self, f, content_len, aligned_len):
+        self.f = f
+        self.content_len = content_len
+        self.align = aligned_len - content_len
+
+    def read(self, sz=65536):
+        if self.content_len == 0:
+            return b""
+        if sz > self.content_len:
+            sz = self.content_len
+        data = self.f.read(sz)
+        sz = len(data)
+        self.content_len -= sz
+        return data
+
+    def readinto(self, buf):
+        if self.content_len == 0:
+            return 0
+        if len(buf) > self.content_len:
+            buf = memoryview(buf)[: self.content_len]
+        sz = self.f.readinto(buf)
+        self.content_len -= sz
+        return sz
+
+    def skip(self):
+        sz = self.content_len + self.align
+        if sz:
+            buf = bytearray(16)
+            while sz:
+                s = min(sz, 16)
+                self.f.readinto(buf, s)
+                sz -= s
+
+
+class TarInfo:
+    def __init__(self, name=""):
+        self.name = name
+        self.mode = _S_IFDIR if self.name[-1] == "/" else _S_IFREG
+
+    @property
+    def type(self):
+        return DIRTYPE if self.isdir() else REGTYPE
+
+    def __str__(self):
+        return "TarInfo(%r, %s, %d)" % (self.name, self.type, self.size)
+
+    def isdir(self):
+        return (self.mode & _S_IFMT) == _S_IFDIR
+
+    def isreg(self):
+        return (self.mode & _S_IFMT) == _S_IFREG
+
+
+class TarFile:
+    def __init__(self, name=None, mode="r", fileobj=None):
+        self.subf = None
+        self.mode = mode
+        self.offset = 0
+        if mode == "r":
+            if fileobj:
+                self.f = fileobj
+            else:
+                self.f = open(name, "rb")
+        else:
+            try:
+                self._open_write(name=name, mode=mode, fileobj=fileobj)
+            except AttributeError:
+                raise NotImplementedError("Install tarfile-write")
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, unused_type, unused_value, unused_traceback):
+        self.close()
+
+    def next(self):
+        if self.subf:
+            self.subf.skip()
+        buf = self.f.read(_BLOCKSIZE)
+        if not buf:
+            return None
+
+        h = uctypes.struct(uctypes.addressof(buf), _TAR_HEADER, uctypes.LITTLE_ENDIAN)
+
+        # Empty block means end of archive
+        if h.name[0] == 0:
+            return None
+
+        # Update the offset once we're sure it's not the run-out.
+        self.offset += len(buf)
+        d = TarInfo(str(h.name, "utf-8").rstrip("\0"))
+        d.size = int(bytes(h.size), 8)
+        self.subf = d.subf = FileSection(self.f, d.size, _roundup(d.size, _BLOCKSIZE))
+        self.offset += _roundup(d.size, _BLOCKSIZE)
+        return d
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        v = self.next()
+        if v is None:
+            raise StopIteration
+        return v
+
+    def extractfile(self, tarinfo):
+        return tarinfo.subf
+
+    def close(self):
+        try:
+            self._close_write()
+        except AttributeError:
+            pass
+        self.f.close()
+
+    # Add additional methods to support write/append from the tarfile-write package.
+    try:
+        from .write import _open_write, _close_write, addfile, add
+    except ImportError:
+        pass
diff --git a/python-stdlib/tempfile/manifest.py b/python-stdlib/tempfile/manifest.py
new file mode 100644
index 000000000..237789248
--- /dev/null
+++ b/python-stdlib/tempfile/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.0.1")
+
+module("tempfile.py")
diff --git a/python-stdlib/tempfile/tempfile.py b/python-stdlib/tempfile/tempfile.py
new file mode 100644
index 000000000..87d889ef6
--- /dev/null
+++ b/python-stdlib/tempfile/tempfile.py
@@ -0,0 +1,59 @@
+import errno
+import os
+import random
+import shutil
+
+_ascii_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+
+def _get_candidate_name(size=8):
+    return "".join(random.choice(_ascii_letters) for _ in range(size))
+
+
+def _sanitize_inputs(suffix, prefix, dir):
+    if dir is None:
+        dir = "/tmp"
+    if suffix is None:
+        suffix = ""
+    if prefix is None:
+        prefix = ""
+    return suffix, prefix, dir
+
+
+def _try(action, *args, **kwargs):
+    try:
+        action(*args, **kwargs)
+        return True
+    except OSError as e:
+        if e.errno != errno.EEXIST:
+            raise e
+    return False
+
+
+def mkdtemp(suffix=None, prefix=None, dir=None):
+    suffix, prefix, dir = _sanitize_inputs(suffix, prefix, dir)
+
+    _try(os.mkdir, dir)
+
+    while True:
+        name = _get_candidate_name()
+        file = dir + "/" + prefix + name + suffix
+        if _try(os.mkdir, file):
+            return file
+
+
+class TemporaryDirectory:
+    def __init__(self, suffix=None, prefix=None, dir=None):
+        self.name = mkdtemp(suffix, prefix, dir)
+
+    def __repr__(self):
+        return "<{} {!r}>".format(self.__class__.__name__, self.name)
+
+    def __enter__(self):
+        return self.name
+
+    def __exit__(self, exc, value, tb):
+        self.cleanup()
+
+    def cleanup(self):
+        _try(shutil.rmtree, self.name)
diff --git a/python-stdlib/tempfile/test_tempfile.py b/python-stdlib/tempfile/test_tempfile.py
new file mode 100644
index 000000000..f9499d187
--- /dev/null
+++ b/python-stdlib/tempfile/test_tempfile.py
@@ -0,0 +1,50 @@
+import os
+import tempfile
+import unittest
+
+
+class Base(unittest.TestCase):
+    def assertExists(self, fn):
+        os.stat(fn)
+
+    def assertNotExists(self, fn):
+        with self.assertRaises(OSError):
+            os.stat(fn)
+
+
+class TestMkdtemp(Base):
+    def test_no_args(self):
+        fn = tempfile.mkdtemp()
+        self.assertTrue(fn.startswith("/tmp"))
+        self.assertExists(fn)
+        os.rmdir(fn)
+
+    def test_prefix(self):
+        fn = tempfile.mkdtemp(prefix="foo")
+        self.assertTrue(fn.startswith("/tmp"))
+        self.assertTrue("foo" in fn)
+        self.assertFalse(fn.endswith("foo"))
+        self.assertExists(fn)
+        os.rmdir(fn)
+
+    def test_suffix(self):
+        fn = tempfile.mkdtemp(suffix="foo")
+        self.assertTrue(fn.startswith("/tmp"))
+        self.assertTrue(fn.endswith("foo"))
+        self.assertExists(fn)
+        os.rmdir(fn)
+
+    def test_dir(self):
+        fn = tempfile.mkdtemp(dir="tmp_micropython")
+        self.assertTrue(fn.startswith("tmp_micropython"))
+        self.assertExists(fn)
+        os.rmdir(fn)
+
+
+class TestTemporaryDirectory(Base):
+    def test_context_manager_no_args(self):
+        with tempfile.TemporaryDirectory() as fn:
+            self.assertTrue(isinstance(fn, str))
+            self.assertTrue(fn.startswith("/tmp"))
+            self.assertExists(fn)
+        self.assertNotExists(fn)
diff --git a/python-stdlib/textwrap/manifest.py b/python-stdlib/textwrap/manifest.py
new file mode 100644
index 000000000..e287ac395
--- /dev/null
+++ b/python-stdlib/textwrap/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="3.4.3")
+
+module("textwrap.py")
diff --git a/textwrap/textwrap.py b/python-stdlib/textwrap/textwrap.py
similarity index 86%
rename from textwrap/textwrap.py
rename to python-stdlib/textwrap/textwrap.py
index 24891804d..4e9f35069 100644
--- a/textwrap/textwrap.py
+++ b/python-stdlib/textwrap/textwrap.py
@@ -7,7 +7,7 @@
 
 import re
 
-__all__ = ['TextWrapper', 'wrap', 'fill', 'dedent', 'indent', 'shorten']
+__all__ = ["TextWrapper", "wrap", "fill", "dedent", "indent", "shorten"]
 
 # Hardcode the recognized whitespace characters to the US-ASCII
 # whitespace characters.  The main reason for doing this is that in
@@ -17,7 +17,8 @@
 # same as any other whitespace char, which is clearly wrong (it's a
 # *non-breaking* space), 2) possibly cause problems with Unicode,
 # since 0xa0 is not in range(128).
-_whitespace = '\t\n\x0b\x0c\r '
+_whitespace = "\t\n\x0b\x0c\r "
+
 
 class TextWrapper:
     """
@@ -69,7 +70,7 @@ class TextWrapper:
     """
 
     unicode_whitespace_trans = {}
-    uspace = ord(' ')
+    uspace = ord(" ")
     for x in _whitespace:
         unicode_whitespace_trans[ord(x)] = uspace
 
@@ -80,38 +81,42 @@ class TextWrapper:
     #   Hello/ /there/ /--/ /you/ /goof-/ball,/ /use/ /the/ /-b/ /option!
     # (after stripping out empty strings).
     wordsep_re = re.compile(
-        r'(\s+|'                                  # any whitespace
-        r'[^\s\w]*\w+[^0-9\W]-(?=\w+[^0-9\W])|'   # hyphenated words
-        r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))')   # em-dash
+        r"(\s+|"  # any whitespace
+        r"[^\s\w]*\w+[^0-9\W]-(?=\w+[^0-9\W])|"  # hyphenated words
+        r"(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))"
+    )  # em-dash
 
     # This less funky little regex just split on recognized spaces. E.g.
     #   "Hello there -- you goof-ball, use the -b option!"
     # splits into
     #   Hello/ /there/ /--/ /you/ /goof-ball,/ /use/ /the/ /-b/ /option!/
-    wordsep_simple_re = re.compile(r'(\s+)')
+    wordsep_simple_re = re.compile(r"(\s+)")
 
     # XXX this is not locale- or charset-aware -- string.lowercase
     # is US-ASCII only (and therefore English-only)
-    sentence_end_re = re.compile(r'[a-z]'             # lowercase letter
-                                 r'[\.\!\?]'          # sentence-ending punct.
-                                 r'[\"\']?'           # optional end-of-quote
-                                 r'\Z')               # end of chunk
-
-
-    def __init__(self,
-                 width=70,
-                 initial_indent="",
-                 subsequent_indent="",
-                 expand_tabs=True,
-                 replace_whitespace=True,
-                 fix_sentence_endings=False,
-                 break_long_words=True,
-                 drop_whitespace=True,
-                 break_on_hyphens=True,
-                 tabsize=8,
-                 *,
-                 max_lines=None,
-                 placeholder=' [...]'):
+    sentence_end_re = re.compile(
+        r"[a-z]"  # lowercase letter
+        r"[\.\!\?]"  # sentence-ending punct.
+        r"[\"\']?"  # optional end-of-quote
+        r"\Z"
+    )  # end of chunk
+
+    def __init__(
+        self,
+        width=70,
+        initial_indent="",
+        subsequent_indent="",
+        expand_tabs=True,
+        replace_whitespace=True,
+        fix_sentence_endings=False,
+        break_long_words=True,
+        drop_whitespace=True,
+        break_on_hyphens=True,
+        tabsize=8,
+        *,
+        max_lines=None,
+        placeholder=" [...]"
+    ):
         self.width = width
         self.initial_indent = initial_indent
         self.subsequent_indent = subsequent_indent
@@ -125,7 +130,6 @@ def __init__(self,
         self.max_lines = max_lines
         self.placeholder = placeholder
 
-
     # -- Private methods -----------------------------------------------
     # (possibly useful for subclasses to override)
 
@@ -142,7 +146,6 @@ def _munge_whitespace(self, text):
             text = text.translate(self.unicode_whitespace_trans)
         return text
 
-
     def _split(self, text):
         """_split(text : string) -> [string]
 
@@ -176,9 +179,9 @@ def _fix_sentence_endings(self, chunks):
         """
         i = 0
         patsearch = self.sentence_end_re.search
-        while i < len(chunks)-1:
-            if chunks[i+1] == " " and patsearch(chunks[i]):
-                chunks[i+1] = "  "
+        while i < len(chunks) - 1:
+            if chunks[i + 1] == " " and patsearch(chunks[i]):
+                chunks[i + 1] = "  "
                 i += 2
             else:
                 i += 1
@@ -245,7 +248,6 @@ def _wrap_chunks(self, chunks):
         chunks.reverse()
 
         while chunks:
-
             # Start the list of chunks that will make up the current line.
             # cur_len is just the length of all the chunks in cur_line.
             cur_line = []
@@ -262,7 +264,7 @@ def _wrap_chunks(self, chunks):
 
             # First chunk on line is whitespace -- drop it, unless this
             # is the very beginning of the text (ie. no lines started yet).
-            if self.drop_whitespace and chunks[-1].strip() == '' and lines:
+            if self.drop_whitespace and chunks[-1].strip() == "" and lines:
                 del chunks[-1]
 
             while chunks:
@@ -284,34 +286,37 @@ def _wrap_chunks(self, chunks):
                 cur_len = sum(map(len, cur_line))
 
             # If the last chunk on this line is all whitespace, drop it.
-            if self.drop_whitespace and cur_line and cur_line[-1].strip() == '':
+            if self.drop_whitespace and cur_line and cur_line[-1].strip() == "":
                 cur_len -= len(cur_line[-1])
                 del cur_line[-1]
 
             if cur_line:
-                if (self.max_lines is None or
-                    len(lines) + 1 < self.max_lines or
-                    (not chunks or
-                     self.drop_whitespace and
-                     len(chunks) == 1 and
-                     not chunks[0].strip()) and cur_len <= width):
+                if (
+                    self.max_lines is None
+                    or len(lines) + 1 < self.max_lines
+                    or (
+                        not chunks
+                        or self.drop_whitespace
+                        and len(chunks) == 1
+                        and not chunks[0].strip()
+                    )
+                    and cur_len <= width
+                ):
                     # Convert current line back to a string and store it in
                     # list of all lines (return value).
-                    lines.append(indent + ''.join(cur_line))
+                    lines.append(indent + "".join(cur_line))
                 else:
                     while cur_line:
-                        if (cur_line[-1].strip() and
-                            cur_len + len(self.placeholder) <= width):
+                        if cur_line[-1].strip() and cur_len + len(self.placeholder) <= width:
                             cur_line.append(self.placeholder)
-                            lines.append(indent + ''.join(cur_line))
+                            lines.append(indent + "".join(cur_line))
                             break
                         cur_len -= len(cur_line[-1])
                         del cur_line[-1]
                     else:
                         if lines:
                             prev_line = lines[-1].rstrip()
-                            if (len(prev_line) + len(self.placeholder) <=
-                                    self.width):
+                            if len(prev_line) + len(self.placeholder) <= self.width:
                                 lines[-1] = prev_line + self.placeholder
                                 break
                         lines.append(indent + self.placeholder.lstrip())
@@ -351,6 +356,7 @@ def fill(self, text):
 
 # -- Convenience interface ---------------------------------------------
 
+
 def wrap(text, width=70, **kwargs):
     """Wrap a single paragraph of text, returning a list of wrapped lines.
 
@@ -364,6 +370,7 @@ def wrap(text, width=70, **kwargs):
     w = TextWrapper(width=width, **kwargs)
     return w.wrap(text)
 
+
 def fill(text, width=70, **kwargs):
     """Fill a single paragraph of text, returning a new string.
 
@@ -376,6 +383,7 @@ def fill(text, width=70, **kwargs):
     w = TextWrapper(width=width, **kwargs)
     return w.fill(text)
 
+
 def shorten(text, width, **kwargs):
     """Collapse and truncate the given text to fit in the given width.
 
@@ -389,13 +397,14 @@ def shorten(text, width, **kwargs):
         'Hello [...]'
     """
     w = TextWrapper(width=width, max_lines=1, **kwargs)
-    return w.fill(' '.join(text.strip().split()))
+    return w.fill(" ".join(text.strip().split()))
 
 
 # -- Loosely related functionality -------------------------------------
 
-_whitespace_only_re = re.compile('^[ \t]+$', re.MULTILINE)
-_leading_whitespace_re = re.compile('(^[ \t]*)(?:[^ \t\n])', re.MULTILINE)
+_whitespace_only_re = re.compile("^[ \t]+$", re.MULTILINE)
+_leading_whitespace_re = re.compile("(^[ \t]*)(?:[^ \t\n])", re.MULTILINE)
+
 
 def dedent(text):
     """Remove any common leading whitespace from every line in `text`.
@@ -413,7 +422,7 @@ def dedent(text):
     # Look for the longest leading string of spaces and tabs common to
     # all lines.
     margin = None
-    text = _whitespace_only_re.sub('', text)
+    text = _whitespace_only_re.sub("", text)
     indents = _leading_whitespace_re.findall(text)
     for indent in indents:
         if margin is None:
@@ -438,11 +447,10 @@ def dedent(text):
     # sanity check (testing/debugging only)
     if 0 and margin:
         for line in text.split("\n"):
-            assert not line or line.startswith(margin), \
-                   "line = %r, margin = %r" % (line, margin)
+            assert not line or line.startswith(margin), "line = %r, margin = %r" % (line, margin)
 
     if margin:
-        text = re.sub(r'(?m)^' + margin, '', text)
+        text = re.sub(r"(?m)^" + margin, "", text)
     return text
 
 
@@ -455,16 +463,18 @@ def indent(text, prefix, predicate=None):
     consist solely of whitespace characters.
     """
     if predicate is None:
+
         def predicate(line):
             return line.strip()
 
     def prefixed_lines():
         for line in text.splitlines(True):
             yield (prefix + line if predicate(line) else line)
-    return ''.join(prefixed_lines())
+
+    return "".join(prefixed_lines())
 
 
 if __name__ == "__main__":
-    #print dedent("\tfoo\n\tbar")
-    #print dedent("  \thello there\n  \t  how are you?")
+    # print dedent("\tfoo\n\tbar")
+    # print dedent("  \thello there\n  \t  how are you?")
     print(dedent("Hello there.\n  This is indented."))
diff --git a/python-stdlib/threading/manifest.py b/python-stdlib/threading/manifest.py
new file mode 100644
index 000000000..8106584be
--- /dev/null
+++ b/python-stdlib/threading/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.0")
+
+module("threading.py")
diff --git a/python-stdlib/threading/threading.py b/python-stdlib/threading/threading.py
new file mode 100644
index 000000000..88af07571
--- /dev/null
+++ b/python-stdlib/threading/threading.py
@@ -0,0 +1,14 @@
+import _thread
+
+
+class Thread:
+    def __init__(self, group=None, target=None, name=None, args=(), kwargs=None):
+        self.target = target
+        self.args = args
+        self.kwargs = {} if kwargs is None else kwargs
+
+    def start(self):
+        _thread.start_new_thread(self.run, ())
+
+    def run(self):
+        self.target(*self.args, **self.kwargs)
diff --git a/python-stdlib/time/README.md b/python-stdlib/time/README.md
new file mode 100644
index 000000000..f07517305
--- /dev/null
+++ b/python-stdlib/time/README.md
@@ -0,0 +1,45 @@
+# time
+
+This library _extends_ the built-in [MicroPython `time`
+module](https://docs.micropython.org/en/latest/library/time.html#module-time) to
+include
+[`time.strftime()`](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior).
+
+`strftime()` is omitted from the built-in `time` module to conserve space.
+
+## Installation
+
+Use `mip` via `mpremote`:
+
+```bash
+> mpremote mip install time
+```
+
+See [Package management](https://docs.micropython.org/en/latest/reference/packages.html) for more details on using `mip` and `mpremote`.
+
+## Common uses
+
+`strftime()` is used when using a loggging [Formatter
+Object](https://docs.python.org/3/library/logging.html#formatter-objects) that
+employs
+[`asctime`](https://docs.python.org/3/library/logging.html#formatter-objects).
+
+For example:
+
+```python
+logging.Formatter('%(asctime)s | %(name)s | %(levelname)s - %(message)s')
+```
+
+The expected output might look like:
+
+```text
+Tue Feb 17 09:42:58 2009 | MAIN | INFO - test
+```
+
+But if this `time` extension library isn't installed, `asctime` will always be
+`None`:
+
+
+```text
+None | MAIN | INFO - test
+```
diff --git a/python-stdlib/time/manifest.py b/python-stdlib/time/manifest.py
new file mode 100644
index 000000000..71af915c4
--- /dev/null
+++ b/python-stdlib/time/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.0")
+
+module("time.py")
diff --git a/python-stdlib/time/test_time.py b/python-stdlib/time/test_time.py
new file mode 100644
index 000000000..98dfcafdb
--- /dev/null
+++ b/python-stdlib/time/test_time.py
@@ -0,0 +1,42 @@
+import time
+import unittest
+
+DAYS = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
+
+MONTHS = (
+    "January",
+    "February",
+    "March",
+    "April",
+    "May",
+    "June",
+    "July",
+    "August",
+    "September",
+    "October",
+    "November",
+    "December",
+)
+
+TIME_TUPLE = (2022, 12, 14, 0, 45, 17, 2, 348, 0)
+
+
+class TestStrftime(unittest.TestCase):
+    def test_not_formatting(self):
+        fmt = "a string with no formatting {}[]() 0123456789 !@#$^&*"
+        self.assertEqual(time.strftime(fmt, TIME_TUPLE), fmt)
+
+    def test_days(self):
+        for i, day in enumerate(DAYS):
+            t = (0, 0, 0, 0, 0, 0, i, 0, 0)
+            self.assertEqual(time.strftime("%a%A", t), day[:3] + day)
+
+    def test_months(self):
+        for i, month in enumerate(MONTHS):
+            t = (0, i + 1, 0, 0, 0, 0, 0, 0, 0)
+            self.assertEqual(time.strftime("%b%B", t), month[:3] + month)
+
+    def test_full(self):
+        fmt = "%Y-%m-%d %a %b %I:%M:%S %%%P%%"
+        expected = "2022-12-14 Wed Dec 00:45:17 %AM%"
+        self.assertEqual(time.strftime(fmt, TIME_TUPLE), expected)
diff --git a/python-stdlib/time/time.py b/python-stdlib/time/time.py
new file mode 100644
index 000000000..68f7d921d
--- /dev/null
+++ b/python-stdlib/time/time.py
@@ -0,0 +1,79 @@
+from utime import *
+from micropython import const
+
+_TS_YEAR = const(0)
+_TS_MON = const(1)
+_TS_MDAY = const(2)
+_TS_HOUR = const(3)
+_TS_MIN = const(4)
+_TS_SEC = const(5)
+_TS_WDAY = const(6)
+_TS_YDAY = const(7)
+_TS_ISDST = const(8)
+
+_WDAY = const(("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"))
+_MDAY = const(
+    (
+        "January",
+        "February",
+        "March",
+        "April",
+        "May",
+        "June",
+        "July",
+        "August",
+        "September",
+        "October",
+        "November",
+        "December",
+    )
+)
+
+
+def strftime(datefmt, ts):
+    from io import StringIO
+
+    fmtsp = False
+    ftime = StringIO()
+    for k in datefmt:
+        if fmtsp:
+            if k == "a":
+                ftime.write(_WDAY[ts[_TS_WDAY]][0:3])
+            elif k == "A":
+                ftime.write(_WDAY[ts[_TS_WDAY]])
+            elif k == "b":
+                ftime.write(_MDAY[ts[_TS_MON] - 1][0:3])
+            elif k == "B":
+                ftime.write(_MDAY[ts[_TS_MON] - 1])
+            elif k == "d":
+                ftime.write("%02d" % ts[_TS_MDAY])
+            elif k == "H":
+                ftime.write("%02d" % ts[_TS_HOUR])
+            elif k == "I":
+                ftime.write("%02d" % (ts[_TS_HOUR] % 12))
+            elif k == "j":
+                ftime.write("%03d" % ts[_TS_YDAY])
+            elif k == "m":
+                ftime.write("%02d" % ts[_TS_MON])
+            elif k == "M":
+                ftime.write("%02d" % ts[_TS_MIN])
+            elif k == "P":
+                ftime.write("AM" if ts[_TS_HOUR] < 12 else "PM")
+            elif k == "S":
+                ftime.write("%02d" % ts[_TS_SEC])
+            elif k == "w":
+                ftime.write(str(ts[_TS_WDAY]))
+            elif k == "y":
+                ftime.write("%02d" % (ts[_TS_YEAR] % 100))
+            elif k == "Y":
+                ftime.write(str(ts[_TS_YEAR]))
+            else:
+                ftime.write(k)
+            fmtsp = False
+        elif k == "%":
+            fmtsp = True
+        else:
+            ftime.write(k)
+    val = ftime.getvalue()
+    ftime.close()
+    return val
diff --git a/python-stdlib/traceback/manifest.py b/python-stdlib/traceback/manifest.py
new file mode 100644
index 000000000..b3ef8343c
--- /dev/null
+++ b/python-stdlib/traceback/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.3.0")
+
+module("traceback.py")
diff --git a/python-stdlib/traceback/traceback.py b/python-stdlib/traceback/traceback.py
new file mode 100644
index 000000000..f55f45762
--- /dev/null
+++ b/python-stdlib/traceback/traceback.py
@@ -0,0 +1,27 @@
+import sys
+
+
+def format_tb(tb, limit):
+    return ["traceback.format_tb() not implemented\n"]
+
+
+def format_exception_only(type, value):
+    return [repr(value) + "\n"]
+
+
+def format_exception(etype, value, tb, limit=None, chain=True):
+    return format_exception_only(etype, value)
+
+
+def print_exception(t, e, tb, limit=None, file=None, chain=True):
+    if file is None:
+        file = sys.stdout
+    sys.print_exception(e, file)
+
+
+def print_exc(limit=None, file=None, chain=True):
+    print_exception(*sys.exc_info(), limit=limit, file=file, chain=chain)
+
+
+def format_exc(limit=None, chain=True):
+    return "".join(format_exception(*sys.exc_info(), limit=limit, chain=chain))
diff --git a/python-stdlib/types/manifest.py b/python-stdlib/types/manifest.py
new file mode 100644
index 000000000..35a47d86c
--- /dev/null
+++ b/python-stdlib/types/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.0.1")
+
+module("types.py")
diff --git a/types/types.py b/python-stdlib/types/types.py
similarity index 80%
rename from types/types.py
rename to python-stdlib/types/types.py
index e1105de60..b1e6cd4e9 100644
--- a/types/types.py
+++ b/python-stdlib/types/types.py
@@ -8,39 +8,56 @@
 # iterator.  Don't check the type!  Use hasattr to check for both
 # "__iter__" and "__next__" attributes instead.
 
-def _f(): pass
+
+def _f():
+    pass
+
+
 FunctionType = type(_f)
-LambdaType = type(lambda: None)         # Same as FunctionType
+LambdaType = type(lambda: None)  # Same as FunctionType
 CodeType = None  # TODO: Add better sentinel which can't match anything
 MappingProxyType = None  # TODO: Add better sentinel which can't match anything
 SimpleNamespace = None  # TODO: Add better sentinel which can't match anything
 
+
 def _g():
     yield 1
+
+
 GeneratorType = type(_g())
 
+
 class _C:
-    def _m(self): pass
+    def _m(self):
+        pass
+
+
 MethodType = type(_C()._m)
 
 BuiltinFunctionType = type(len)
-BuiltinMethodType = type([].append)     # Same as BuiltinFunctionType
+BuiltinMethodType = type([].append)  # Same as BuiltinFunctionType
 
 ModuleType = type(sys)
 
 try:
     raise TypeError
 except TypeError:
-#    tb = sys.exc_info()[2]
+    #    tb = sys.exc_info()[2]
     TracebackType = None  # TODO: Add better sentinel which can't match anything
     FrameType = None  # TODO: Add better sentinel which can't match anything
-    tb = None; del tb
+    tb = None
+    del tb
 
 # For Jython, the following two types are identical
 GetSetDescriptorType = None  # TODO: Add better sentinel which can't match anything
 MemberDescriptorType = None  # TODO: Add better sentinel which can't match anything
 
-del sys, _f, _g, _C,                              # Not for export
+del (
+    sys,
+    _f,
+    _g,
+    _C,
+)  # Not for export
 
 
 # Provide a PEP 3115 compliant mechanism for class creation
@@ -51,6 +68,7 @@ def new_class(name, bases=(), kwds=None, exec_body=None):
         exec_body(ns)
     return meta(name, bases, ns, **kwds)
 
+
 def prepare_class(name, bases=(), kwds=None):
     """Call the __prepare__ method of the appropriate metaclass.
 
@@ -65,9 +83,9 @@ def prepare_class(name, bases=(), kwds=None):
     if kwds is None:
         kwds = {}
     else:
-        kwds = dict(kwds) # Don't alter the provided mapping
-    if 'metaclass' in kwds:
-        meta = kwds.pop('metaclass')
+        kwds = dict(kwds)  # Don't alter the provided mapping
+    if "metaclass" in kwds:
+        meta = kwds.pop("metaclass")
     else:
         if bases:
             meta = type(bases[0])
@@ -77,12 +95,13 @@ def prepare_class(name, bases=(), kwds=None):
         # when meta is a type, we first determine the most-derived metaclass
         # instead of invoking the initial candidate directly
         meta = _calculate_meta(meta, bases)
-    if hasattr(meta, '__prepare__'):
+    if hasattr(meta, "__prepare__"):
         ns = meta.__prepare__(name, bases, **kwds)
     else:
         ns = {}
     return meta, ns, kwds
 
+
 def _calculate_meta(meta, bases):
     """Calculate the most derived metaclass."""
     winner = meta
@@ -94,8 +113,10 @@ def _calculate_meta(meta, bases):
             winner = base_meta
             continue
         # else:
-        raise TypeError("metaclass conflict: "
-                        "the metaclass of a derived class "
-                        "must be a (non-strict) subclass "
-                        "of the metaclasses of all its bases")
+        raise TypeError(
+            "metaclass conflict: "
+            "the metaclass of a derived class "
+            "must be a (non-strict) subclass "
+            "of the metaclasses of all its bases"
+        )
     return winner
diff --git a/python-stdlib/unittest-discover/manifest.py b/python-stdlib/unittest-discover/manifest.py
new file mode 100644
index 000000000..5610f41e2
--- /dev/null
+++ b/python-stdlib/unittest-discover/manifest.py
@@ -0,0 +1,7 @@
+metadata(version="0.1.3")
+
+require("argparse")
+require("fnmatch")
+require("unittest")
+
+package("unittest")
diff --git a/python-stdlib/unittest-discover/tests/isolated.py b/python-stdlib/unittest-discover/tests/isolated.py
new file mode 100644
index 000000000..21b905c15
--- /dev/null
+++ b/python-stdlib/unittest-discover/tests/isolated.py
@@ -0,0 +1,4 @@
+# Module that is used in both test_isolated_1.py and test_isolated_2.py.
+# The module should be clean reloaded for each.
+
+state = None
diff --git a/python-stdlib/unittest-discover/tests/sub/sub.py b/python-stdlib/unittest-discover/tests/sub/sub.py
new file mode 100644
index 000000000..b6614dd63
--- /dev/null
+++ b/python-stdlib/unittest-discover/tests/sub/sub.py
@@ -0,0 +1 @@
+imported = True
diff --git a/python-stdlib/unittest-discover/tests/sub/test_module_import.py b/python-stdlib/unittest-discover/tests/sub/test_module_import.py
new file mode 100644
index 000000000..5c6404d6f
--- /dev/null
+++ b/python-stdlib/unittest-discover/tests/sub/test_module_import.py
@@ -0,0 +1,13 @@
+import sys
+import unittest
+
+
+class TestModuleImport(unittest.TestCase):
+    def test_ModuleImportPath(self):
+        try:
+            from sub.sub import imported
+            assert imported
+        except ImportError:
+            print("This test is intended to be run with unittest discover"
+                  "from the unittest-discover/tests dir. sys.path:", sys.path)
+            raise
diff --git a/python-stdlib/unittest-discover/tests/test_isolated_1.py b/python-stdlib/unittest-discover/tests/test_isolated_1.py
new file mode 100644
index 000000000..a2bd83b11
--- /dev/null
+++ b/python-stdlib/unittest-discover/tests/test_isolated_1.py
@@ -0,0 +1,8 @@
+import unittest
+import isolated
+
+
+class TestUnittestIsolated1(unittest.TestCase):
+    def test_NotChangedByOtherTest(self):
+        self.assertIsNone(isolated.state)
+        isolated.state = True
diff --git a/python-stdlib/unittest-discover/tests/test_isolated_2.py b/python-stdlib/unittest-discover/tests/test_isolated_2.py
new file mode 100644
index 000000000..1beb99618
--- /dev/null
+++ b/python-stdlib/unittest-discover/tests/test_isolated_2.py
@@ -0,0 +1,8 @@
+import unittest
+import isolated
+
+
+class TestUnittestIsolated2(unittest.TestCase):
+    def test_NotChangedByOtherTest(self):
+        self.assertIsNone(isolated.state)
+        isolated.state = True
diff --git a/python-stdlib/unittest-discover/unittest/__main__.py b/python-stdlib/unittest-discover/unittest/__main__.py
new file mode 100644
index 000000000..09dfd03b9
--- /dev/null
+++ b/python-stdlib/unittest-discover/unittest/__main__.py
@@ -0,0 +1,153 @@
+# Extension for "unittest" that adds the ability to run via "micropython -m unittest".
+
+import argparse
+import os
+import sys
+from fnmatch import fnmatch
+from micropython import const
+
+try:
+    from unittest import TestRunner, TestResult, TestSuite
+except ImportError:
+    print("Error: This must be used from an installed copy of unittest-discover which will"
+          " also install base unittest module.")
+    raise
+
+
+# Run a single test in a clean environment.
+def _run_test_module(runner: TestRunner, module_name: str, *extra_paths: list[str]):
+    module_snapshot = {k: v for k, v in sys.modules.items()}
+    path_snapshot = sys.path[:]
+    try:
+        for path in extra_paths:
+            if path:
+                sys.path.insert(0, path)
+
+        module = __import__(module_name, None, None, module_name)
+        suite = TestSuite(module_name)
+        suite._load_module(module)
+        return runner.run(suite)
+    finally:
+        sys.path[:] = path_snapshot
+        sys.modules.clear()
+        sys.modules.update(module_snapshot)
+
+
+_DIR_TYPE = const(0x4000)
+
+
+def _run_all_in_dir(runner: TestRunner, path: str, pattern: str, top: str):
+    result = TestResult()
+    for fname, ftype, *_ in os.ilistdir(path):
+        if fname in ("..", "."):
+            continue
+        fpath = "/".join((path, fname))
+        if ftype == _DIR_TYPE:
+            result += _run_all_in_dir(
+                runner=runner,
+                path=fpath,
+                pattern=pattern,
+                top=top,
+            )
+        if fnmatch(fname, pattern):
+            module_path = fpath.rsplit(".", 1)[0]  # remove ext
+            module_path = module_path.replace("/", ".").strip(".")
+            result += _run_test_module(runner, module_path, top)
+    return result
+
+
+# Implements discovery inspired by https://docs.python.org/3/library/unittest.html#test-discovery
+def _discover(runner: TestRunner):
+    parser = argparse.ArgumentParser()
+    # parser.add_argument(
+    #     "-v",
+    #     "--verbose",
+    #     action="store_true",
+    #     help="Verbose output",
+    # )
+    parser.add_argument(
+        "-s",
+        "--start-directory",
+        dest="start",
+        default=".",
+        help="Directory to start discovery",
+    )
+    parser.add_argument(
+        "-p",
+        "--pattern ",
+        dest="pattern",
+        default="test*.py",
+        help="Pattern to match test files",
+    )
+    parser.add_argument(
+        "-t",
+        "--top-level-directory",
+        dest="top",
+        help="Top level directory of project (defaults to start directory)",
+    )
+    args = parser.parse_args(args=sys.argv[2:])
+
+    path = args.start
+    top = args.top or path
+
+    return _run_all_in_dir(
+        runner=runner,
+        path=path,
+        pattern=args.pattern,
+        top=top,
+    )
+
+
+# TODO: Use os.path for path handling.
+PATH_SEP = getattr(os, "sep", "/")
+
+
+# foo/bar/x.y.z --> foo/bar, x.y
+def _dirname_filename_no_ext(path):
+    # Workaround: The Windows port currently reports "/" for os.sep
+    # (and MicroPython doesn't have os.altsep). So for now just
+    # always work with os.sep (i.e. "/").
+    path = path.replace("\\", PATH_SEP)
+
+    split = path.rsplit(PATH_SEP, 1)
+    if len(split) == 1:
+        dirname, filename = "", split[0]
+    else:
+        dirname, filename = split
+    return dirname, filename.rsplit(".", 1)[0]
+
+
+def discover_main():
+    runner = TestRunner()
+
+    if len(sys.argv) == 1 or (
+        len(sys.argv) >= 2
+        and _dirname_filename_no_ext(sys.argv[0])[1] == "unittest"
+        and sys.argv[1] == "discover"
+    ):
+        # No args, or `python -m unittest discover ...`.
+        result = _discover(runner)
+    else:
+        result = TestResult()
+        for test_spec in sys.argv[1:]:
+            try:
+                os.stat(test_spec)
+                # File exists, strip extension and import with its parent directory in sys.path.
+                dirname, module_name = _dirname_filename_no_ext(test_spec)
+                res = _run_test_module(runner, module_name, dirname)
+            except OSError:
+                # Not a file, treat as named module to import.
+                res = _run_test_module(runner, test_spec)
+
+            result += res
+
+    if not result.testsRun:
+        # If tests are run their results are already printed.
+        # Ensure an appropriate output is printed if no tests are found.
+        runner.run(TestSuite())
+
+    # Terminate with non zero return code in case of failures.
+    sys.exit(result.failuresNum + result.errorsNum)
+
+
+discover_main()
diff --git a/python-stdlib/unittest/examples/example_subtest.py b/python-stdlib/unittest/examples/example_subtest.py
new file mode 100644
index 000000000..558af0b26
--- /dev/null
+++ b/python-stdlib/unittest/examples/example_subtest.py
@@ -0,0 +1,214 @@
+"""Tests using unittest.subtest as an example reference for how it is used"""
+
+import unittest
+
+
+def factorial(value: int) -> int:
+    """Iterative factorial algorithm implementation"""
+    result = 1
+
+    for i in range(1, value + 1):
+        result *= i
+
+    return result
+
+
+class Person:
+    """Represents a person with a name, age, and who can make friends"""
+
+    def __init__(self, name: str, age: int) -> None:
+        self.name = name
+        self.age = age
+        self.friends = set()
+
+    def __repr__(self) -> str:
+        return f"Person({self.name})"
+
+    def add_friend(self, friend):
+        """Logs that this Person has made a new friend"""
+        self.friends.add(friend)
+
+    def has_friend(self, friend_candidate) -> bool:
+        """Determines if this Person has the friend `friend_candidate`"""
+        return friend_candidate in self.friends
+
+    def is_oldest_friend(self) -> bool:
+        """Determines whether this Person is the oldest out of themself and their friends"""
+        return self.age > max(friend.age for friend in self.friends)
+
+
+class TestSubtest(unittest.TestCase):
+    """Examples/tests of unittest.subTest()"""
+
+    def test_sorted(self) -> None:
+        """Test that the selection sort function correctly sorts lists"""
+        tests = [
+            {
+                "unsorted": [-68, 15, 52, -54, -64, 20, 2, 66, 33],
+                "sorted": [-68, -64, -54, 2, 15, 20, 33, 52, 66],
+                "correct": True,
+            },
+            {
+                "unsorted": [-68, 15, 52, -54, -64, 20, 2, 66, 33],
+                "sorted": [-68, -54, -64, 2, 15, 20, 33, 52, 66],
+                "correct": False,
+            },
+            {
+                "unsorted": [-68, 15, 52, 54, -64, 20, 2, 66, 33],
+                "sorted": [-68, -64, -54, 2, 15, 20, 33, 52, 66],
+                "correct": False,
+            },
+            {
+                "unsorted": [],
+                "sorted": [],
+                "correct": True,
+            },
+            {
+                "unsorted": [42],
+                "sorted": [42],
+                "correct": True,
+            },
+            {
+                "unsorted": [42],
+                "sorted": [],
+                "correct": False,
+            },
+            {
+                "unsorted": [],
+                "sorted": [24],
+                "correct": False,
+            },
+            {
+                "unsorted": [43, 44],
+                "sorted": [43, 44],
+                "correct": True,
+            },
+            {
+                "unsorted": [44, 43],
+                "sorted": [43, 44],
+                "correct": True,
+            },
+        ]
+
+        for test in tests:
+            with self.subTest():  # Subtests continue to be tested, even if an earlier one fails
+                if test["correct"]:  # Tests that match what is expected
+                    self.assertEqual(sorted(test["unsorted"]), test["sorted"])
+                else:  # Tests that are meant to fail
+                    with self.assertRaises(AssertionError):
+                        self.assertEqual(sorted(test["unsorted"]), test["sorted"])
+
+    def test_factorial(self) -> None:
+        """Test that the factorial fuction correctly calculates factorials
+
+        Makes use of `msg` argument in subtest method to clarify which subtests had an
+        error in the results
+        """
+        tests = [
+            {
+                "operand": 0,
+                "result": 1,
+                "correct": True,
+            },
+            {
+                "operand": 1,
+                "result": 1,
+                "correct": True,
+            },
+            {
+                "operand": 1,
+                "result": 0,
+                "correct": False,
+            },
+            {
+                "operand": 2,
+                "result": 2,
+                "correct": True,
+            },
+            {
+                "operand": 3,
+                "result": 6,
+                "correct": True,
+            },
+            {
+                "operand": 3,
+                "result": -6,
+                "correct": False,
+            },
+            {
+                "operand": 4,
+                "result": 24,
+                "correct": True,
+            },
+            {
+                "operand": 15,
+                "result": 1_307_674_368_000,
+                "correct": True,
+            },
+            {
+                "operand": 15,
+                "result": 1_307_674_368_001,
+                "correct": False,
+            },
+            {
+                "operand": 11,
+                "result": 39_916_800,
+                "correct": True,
+            },
+        ]
+
+        for test in tests:
+            with self.subTest(
+                f"{test['operand']}!"
+            ):  # Let's us know we were testing "x!" when we get an error
+                if test["correct"]:
+                    self.assertEqual(factorial(test["operand"]), test["result"])
+                else:
+                    with self.assertRaises(AssertionError):
+                        self.assertEqual(factorial(test["operand"]), test["result"])
+
+    def test_person(self) -> None:
+        """Test the Person class and its friend-making ability
+
+        Makes use of subtest's params to specify relevant data about the tests, which is
+        helpful for debugging
+        """
+        # Create a friendship
+        alice = Person("Alice", 22)
+        bob = Person("Bob", 23)
+        alice.add_friend(bob)
+
+        # Test friendship init
+        with self.subTest(
+            "Alice should have Bob as a friend", name=alice.name, friends=bob.friends
+        ):  # Params `name` and `friends` provide useful data for debugging purposes
+            self.assertTrue(alice.has_friend(bob))
+
+        with self.subTest(
+            "Bob should not have Alice as a friend", name=bob.name, friends=bob.friends
+        ):
+            self.assertFalse(bob.has_friend(alice))
+
+        # Friendship is not always  commutative, so Bob is not implicitly friends with Alice
+        with self.subTest("Alice and Bob should not both be friends with eachother"):
+            with self.assertRaises(AssertionError):
+                self.assertTrue(bob.has_friend(alice) and alice.has_friend(bob))
+
+        # Bob becomes friends with Alice
+        bob.add_friend(alice)
+
+        with self.subTest(
+            "Bob should now have Alice as a friend", name=bob.name, friends=bob.friends
+        ):
+            self.assertTrue(bob.has_friend(alice))
+
+        with self.subTest(
+            "Bob should be the oldest of his friends",
+            age=bob.age,
+            friend_ages=[friend.age for friend in bob.friends],
+        ):  # Different params can be used for different subtests in the same test
+            self.assertTrue(bob.is_oldest_friend())
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python-stdlib/unittest/manifest.py b/python-stdlib/unittest/manifest.py
new file mode 100644
index 000000000..a01bbb8e6
--- /dev/null
+++ b/python-stdlib/unittest/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.10.4")
+
+package("unittest")
diff --git a/python-stdlib/unittest/tests/exception.py b/python-stdlib/unittest/tests/exception.py
new file mode 100644
index 000000000..0e828e226
--- /dev/null
+++ b/python-stdlib/unittest/tests/exception.py
@@ -0,0 +1,15 @@
+# This makes unittest return an error code, so is not named "test_xxx.py".
+
+import unittest
+
+
+def broken_func():
+    raise ValueError("uh oh!")
+
+
+def test_func():
+    broken_func()
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python-stdlib/unittest/tests/test_assertions.py b/python-stdlib/unittest/tests/test_assertions.py
new file mode 100644
index 000000000..b191220e6
--- /dev/null
+++ b/python-stdlib/unittest/tests/test_assertions.py
@@ -0,0 +1,154 @@
+import unittest
+
+
+class TestUnittestAssertions(unittest.TestCase):
+    def testFail(self):
+        with self.assertRaises(AssertionError):
+            self.fail("failure")
+
+    def testEqual(self):
+        self.assertEqual(0, 0)
+        self.assertEqual([0, 1, 2], [0, 1, 2])
+        with self.assertRaises(AssertionError):
+            self.assertEqual(0, None)
+        with self.assertRaises(AssertionError):
+            self.assertEqual([0, 1, 2], [1, 2, 3])
+
+    def test_AlmostEqual(self):
+        self.assertAlmostEqual(1.00000001, 1.0)
+        self.assertNotAlmostEqual(1.0000001, 1.0)
+        with self.assertRaises(AssertionError):
+            self.assertAlmostEqual(1.0000001, 1.0)
+        with self.assertRaises(AssertionError):
+            self.assertNotAlmostEqual(1.00000001, 1.0)
+
+        self.assertAlmostEqual(1.1, 1.0, places=0)
+        with self.assertRaises(AssertionError):
+            self.assertAlmostEqual(1.1, 1.0, places=1)
+
+        self.assertAlmostEqual(0, 0.1 + 0.1j, places=0)
+        self.assertNotAlmostEqual(0, 0.1 + 0.1j, places=1)
+        with self.assertRaises(AssertionError):
+            self.assertAlmostEqual(0, 0.1 + 0.1j, places=1)
+        with self.assertRaises(AssertionError):
+            self.assertNotAlmostEqual(0, 0.1 + 0.1j, places=0)
+
+        self.assertAlmostEqual(float("inf"), float("inf"))
+        with self.assertRaises(AssertionError):
+            self.assertNotAlmostEqual(float("inf"), float("inf"))
+
+    def test_AlmostEqualWithDelta(self):
+        self.assertAlmostEqual(1.1, 1.0, delta=0.5)
+        self.assertAlmostEqual(1.0, 1.1, delta=0.5)
+        self.assertNotAlmostEqual(1.1, 1.0, delta=0.05)
+        self.assertNotAlmostEqual(1.0, 1.1, delta=0.05)
+
+        self.assertAlmostEqual(1.0, 1.0, delta=0.5)
+        with self.assertRaises(AssertionError):
+            self.assertNotAlmostEqual(1.0, 1.0, delta=0.5)
+        with self.assertRaises(AssertionError):
+            self.assertAlmostEqual(1.1, 1.0, delta=0.05)
+        with self.assertRaises(AssertionError):
+            self.assertNotAlmostEqual(1.1, 1.0, delta=0.5)
+        with self.assertRaises(TypeError):
+            self.assertAlmostEqual(1.1, 1.0, places=2, delta=2)
+        with self.assertRaises(TypeError):
+            self.assertNotAlmostEqual(1.1, 1.0, places=2, delta=2)
+
+    def testNotEqual(self):
+        self.assertNotEqual([0, 1, 2], [0, 2, 1])
+        with self.assertRaises(AssertionError):
+            self.assertNotEqual(0, 0)
+        with self.assertRaises(AssertionError):
+            self.assertNotEqual([0, 1, 2], [0, 1, 2])
+
+    def testIs(self):
+        self.assertIs(None, None)
+        with self.assertRaises(AssertionError):
+            self.assertIs([1, 2, 3], [1, 2, 3])
+
+    def testIsNot(self):
+        self.assertIsNot([1, 2, 3], [1, 2, 3])
+        with self.assertRaises(AssertionError):
+            self.assertIsNot(None, None)
+
+    def testIsNone(self):
+        self.assertIsNone(None)
+        with self.assertRaises(AssertionError):
+            self.assertIsNone(0)
+
+    def testIsNotNone(self):
+        self.assertIsNotNone(0)
+        with self.assertRaises(AssertionError):
+            self.assertIsNotNone(None)
+
+    def testTrue(self):
+        self.assertTrue(True)
+        with self.assertRaises(AssertionError):
+            self.assertTrue(False)
+
+    def testFalse(self):
+        self.assertFalse(False)
+        with self.assertRaises(AssertionError):
+            self.assertFalse(True)
+
+    def testIn(self):
+        self.assertIn("t", "cat")
+        with self.assertRaises(AssertionError):
+            self.assertIn("x", "cat")
+
+    def testIsInstance(self):
+        self.assertIsInstance("cat", str)
+        with self.assertRaises(AssertionError):
+            self.assertIsInstance(7, str)
+
+    def testRaises(self):
+        with self.assertRaises(ZeroDivisionError):
+            1 / 0
+            pass
+
+    @unittest.skip("test of skipping")
+    def testSkip(self):
+        self.fail("this should be skipped")
+
+    def testAssert(self):
+        e1 = None
+        try:
+
+            def func_under_test(a):
+                assert a > 10
+
+            self.assertRaises(AssertionError, func_under_test, 20)
+        except AssertionError as e:
+            e1 = e
+
+        if not e1 or "not raised" not in e1.args[0]:
+            self.fail("Expected to catch lack of AssertionError from assert in func_under_test")
+
+    @unittest.expectedFailure
+    def testExpectedFailure(self):
+        self.assertEqual(1, 0)
+
+    def testExpectedFailureNot(self):
+        @unittest.expectedFailure
+        def testInner():
+            self.assertEqual(1, 1)
+
+        try:
+            testInner()
+        except:
+            pass
+        else:
+            self.fail("Unexpected success was not detected")
+
+    def test_subtest_even(self):
+        """
+        Test that numbers between 0 and 5 are all even.
+        """
+        for i in range(0, 10, 2):
+            with self.subTest("Should only pass for even numbers", i=i):
+                self.assertEqual(i % 2, 0)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python-stdlib/unittest/tests/test_basics.py b/python-stdlib/unittest/tests/test_basics.py
new file mode 100644
index 000000000..70f0dd616
--- /dev/null
+++ b/python-stdlib/unittest/tests/test_basics.py
@@ -0,0 +1,29 @@
+import unittest
+
+
+class TestWithRunTest(unittest.TestCase):
+    run = False
+
+    def runTest(self):
+        TestWithRunTest.run = True
+
+    def testRunTest(self):
+        self.fail()
+
+    @staticmethod
+    def tearDownClass():
+        if not TestWithRunTest.run:
+            raise ValueError()
+
+
+def test_func():
+    pass
+
+
+@unittest.expectedFailure
+def test_foo():
+    raise ValueError()
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python-stdlib/unittest/tests/test_setup.py b/python-stdlib/unittest/tests/test_setup.py
new file mode 100644
index 000000000..43fdcece7
--- /dev/null
+++ b/python-stdlib/unittest/tests/test_setup.py
@@ -0,0 +1,28 @@
+import unittest
+
+
+class TestUnittestSetup(unittest.TestCase):
+    class_setup_var = 0
+
+    @classmethod
+    def setUpClass(cls):
+        assert cls is TestUnittestSetup
+        TestUnittestSetup.class_setup_var += 1
+
+    @classmethod
+    def tearDownClass(cls):
+        assert cls is TestUnittestSetup
+        # Not sure how to actually test this, but we can check (in the test case below)
+        # that it hasn't been run already at least.
+        TestUnittestSetup.class_setup_var = -1
+
+    def testSetUpTearDownClass_1(self):
+        assert TestUnittestSetup.class_setup_var == 1, TestUnittestSetup.class_setup_var
+
+    def testSetUpTearDownClass_2(self):
+        # Test this twice, as if setUpClass() gets run like setUp() it would be run twice
+        assert TestUnittestSetup.class_setup_var == 1, TestUnittestSetup.class_setup_var
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python-stdlib/unittest/tests/test_subtest.py b/python-stdlib/unittest/tests/test_subtest.py
new file mode 100644
index 000000000..324150e27
--- /dev/null
+++ b/python-stdlib/unittest/tests/test_subtest.py
@@ -0,0 +1,14 @@
+import unittest
+
+
+class Test(unittest.TestCase):
+    def test_subtest_skip(self):
+        for i in range(4):
+            with self.subTest(i=i):
+                print("sub test", i)
+                if i == 2:
+                    self.skipTest("skip 2")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python-stdlib/unittest/unittest/__init__.py b/python-stdlib/unittest/unittest/__init__.py
new file mode 100644
index 000000000..61b315788
--- /dev/null
+++ b/python-stdlib/unittest/unittest/__init__.py
@@ -0,0 +1,464 @@
+import io
+import os
+import sys
+
+try:
+    import traceback
+except ImportError:
+    traceback = None
+
+
+class SkipTest(Exception):
+    pass
+
+
+class AssertRaisesContext:
+    def __init__(self, exc):
+        self.expected = exc
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, tb):
+        self.exception = exc_value
+        if exc_type is None:
+            assert False, "%r not raised" % self.expected
+        if issubclass(exc_type, self.expected):
+            # store exception for later retrieval
+            self.exception = exc_value
+            return True
+        return False
+
+
+# These are used to provide required context to things like subTest
+__current_test__ = None
+__test_result__ = None
+
+
+class SubtestContext:
+    def __init__(self, msg=None, params=None):
+        self.msg = msg
+        self.params = params
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, *exc_info):
+        if exc_info[0] is not None:
+            # Exception raised
+            global __test_result__, __current_test__
+            test_details = __current_test__
+            if self.msg:
+                test_details += (f" [{self.msg}]",)
+            if self.params:
+                detail = ", ".join(f"{k}={v}" for k, v in self.params.items())
+                test_details += (f" ({detail})",)
+
+            _handle_test_exception(test_details, __test_result__, exc_info, False)
+        # Suppress the exception as we've captured it above
+        return True
+
+
+class NullContext:
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        pass
+
+
+class TestCase:
+    def __init__(self):
+        pass
+
+    def addCleanup(self, func, *args, **kwargs):
+        if not hasattr(self, "_cleanups"):
+            self._cleanups = []
+        self._cleanups.append((func, args, kwargs))
+
+    def doCleanups(self):
+        if hasattr(self, "_cleanups"):
+            while self._cleanups:
+                func, args, kwargs = self._cleanups.pop()
+                func(*args, **kwargs)
+
+    def subTest(self, msg=None, **params):
+        return SubtestContext(msg=msg, params=params)
+
+    def skipTest(self, reason):
+        raise SkipTest(reason)
+
+    def fail(self, msg=""):
+        assert False, msg
+
+    def assertEqual(self, x, y, msg=""):
+        if not msg:
+            msg = "%r vs (expected) %r" % (x, y)
+        assert x == y, msg
+
+    def assertNotEqual(self, x, y, msg=""):
+        if not msg:
+            msg = "%r not expected to be equal %r" % (x, y)
+        assert x != y, msg
+
+    def assertLessEqual(self, x, y, msg=None):
+        if msg is None:
+            msg = "%r is expected to be <= %r" % (x, y)
+        assert x <= y, msg
+
+    def assertGreaterEqual(self, x, y, msg=None):
+        if msg is None:
+            msg = "%r is expected to be >= %r" % (x, y)
+        assert x >= y, msg
+
+    def assertAlmostEqual(self, x, y, places=None, msg="", delta=None):
+        if x == y:
+            return
+        if delta is not None and places is not None:
+            raise TypeError("specify delta or places not both")
+
+        if delta is not None:
+            if abs(x - y) <= delta:
+                return
+            if not msg:
+                msg = "%r != %r within %r delta" % (x, y, delta)
+        else:
+            if places is None:
+                places = 7
+            if round(abs(y - x), places) == 0:
+                return
+            if not msg:
+                msg = "%r != %r within %r places" % (x, y, places)
+
+        assert False, msg
+
+    def assertNotAlmostEqual(self, x, y, places=None, msg="", delta=None):
+        if delta is not None and places is not None:
+            raise TypeError("specify delta or places not both")
+
+        if delta is not None:
+            if not (x == y) and abs(x - y) > delta:
+                return
+            if not msg:
+                msg = "%r == %r within %r delta" % (x, y, delta)
+        else:
+            if places is None:
+                places = 7
+            if not (x == y) and round(abs(y - x), places) != 0:
+                return
+            if not msg:
+                msg = "%r == %r within %r places" % (x, y, places)
+
+        assert False, msg
+
+    def assertIs(self, x, y, msg=""):
+        if not msg:
+            msg = "%r is not %r" % (x, y)
+        assert x is y, msg
+
+    def assertIsNot(self, x, y, msg=""):
+        if not msg:
+            msg = "%r is %r" % (x, y)
+        assert x is not y, msg
+
+    def assertIsNone(self, x, msg=""):
+        if not msg:
+            msg = "%r is not None" % x
+        assert x is None, msg
+
+    def assertIsNotNone(self, x, msg=""):
+        if not msg:
+            msg = "%r is None" % x
+        assert x is not None, msg
+
+    def assertTrue(self, x, msg=""):
+        if not msg:
+            msg = "Expected %r to be True" % x
+        assert x, msg
+
+    def assertFalse(self, x, msg=""):
+        if not msg:
+            msg = "Expected %r to be False" % x
+        assert not x, msg
+
+    def assertIn(self, x, y, msg=""):
+        if not msg:
+            msg = "Expected %r to be in %r" % (x, y)
+        assert x in y, msg
+
+    def assertIsInstance(self, x, y, msg=""):
+        assert isinstance(x, y), msg
+
+    def assertRaises(self, exc, func=None, *args, **kwargs):
+        if func is None:
+            return AssertRaisesContext(exc)
+
+        try:
+            func(*args, **kwargs)
+        except Exception as e:
+            if isinstance(e, exc):
+                return
+            raise e
+
+        assert False, "%r not raised" % exc
+
+    def assertWarns(self, warn):
+        return NullContext()
+
+
+def skip(msg):
+    def _decor(fun):
+        # We just replace original fun with _inner
+        def _inner(self):
+            raise SkipTest(msg)
+
+        return _inner
+
+    return _decor
+
+
+def skipIf(cond, msg):
+    if not cond:
+        return lambda x: x
+    return skip(msg)
+
+
+def skipUnless(cond, msg):
+    if cond:
+        return lambda x: x
+    return skip(msg)
+
+
+def expectedFailure(test):
+    def test_exp_fail(*args, **kwargs):
+        try:
+            test(*args, **kwargs)
+        except:
+            pass
+        else:
+            assert False, "unexpected success"
+
+    return test_exp_fail
+
+
+class TestSuite:
+    def __init__(self, name=""):
+        self._tests = []
+        self.name = name
+
+    def addTest(self, cls):
+        self._tests.append(cls)
+
+    def run(self, result):
+        for c in self._tests:
+            _run_suite(c, result, self.name)
+        return result
+
+    def _load_module(self, mod):
+        for tn in dir(mod):
+            c = getattr(mod, tn)
+            if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase):
+                self.addTest(c)
+            elif tn.startswith("test") and callable(c):
+                self.addTest(c)
+
+
+class TestRunner:
+    def run(self, suite: TestSuite):
+        res = TestResult()
+        suite.run(res)
+
+        res.printErrors()
+        print("----------------------------------------------------------------------")
+        print("Ran %d tests\n" % res.testsRun)
+        if res.failuresNum > 0 or res.errorsNum > 0:
+            print("FAILED (failures=%d, errors=%d)" % (res.failuresNum, res.errorsNum))
+        else:
+            msg = "OK"
+            if res.skippedNum > 0:
+                msg += " (skipped=%d)" % res.skippedNum
+            print(msg)
+
+        return res
+
+
+TextTestRunner = TestRunner
+
+
+class TestResult:
+    def __init__(self):
+        self.errorsNum = 0
+        self.failuresNum = 0
+        self.skippedNum = 0
+        self.testsRun = 0
+        self.errors = []
+        self.failures = []
+        self.skipped = []
+        self._newFailures = 0
+
+    def wasSuccessful(self):
+        return self.errorsNum == 0 and self.failuresNum == 0
+
+    def printErrors(self):
+        if self.errors or self.failures:
+            print()
+            self.printErrorList(self.errors)
+            self.printErrorList(self.failures)
+
+    def printErrorList(self, lst):
+        sep = "----------------------------------------------------------------------"
+        for c, e in lst:
+            detail = " ".join((str(i) for i in c))
+            print("======================================================================")
+            print(f"FAIL: {detail}")
+            print(sep)
+            print(e)
+
+    def __repr__(self):
+        # Format is compatible with CPython.
+        return "<unittest.result.TestResult run=%d errors=%d failures=%d>" % (
+            self.testsRun,
+            self.errorsNum,
+            self.failuresNum,
+        )
+
+    def __add__(self, other):
+        self.errorsNum += other.errorsNum
+        self.failuresNum += other.failuresNum
+        self.skippedNum += other.skippedNum
+        self.testsRun += other.testsRun
+        self.errors.extend(other.errors)
+        self.failures.extend(other.failures)
+        self.skipped.extend(other.skipped)
+        return self
+
+
+def _capture_exc(exc, exc_traceback):
+    buf = io.StringIO()
+    if hasattr(sys, "print_exception"):
+        sys.print_exception(exc, buf)
+    elif traceback is not None:
+        traceback.print_exception(None, exc, exc_traceback, file=buf)
+    return buf.getvalue()
+
+
+def _handle_test_exception(
+    current_test: tuple, test_result: TestResult, exc_info: tuple, verbose=True
+):
+    exc = exc_info[1]
+    traceback = exc_info[2]
+    ex_str = _capture_exc(exc, traceback)
+    if isinstance(exc, SkipTest):
+        reason = exc.args[0]
+        test_result.skippedNum += 1
+        test_result.skipped.append((current_test, reason))
+        print(" skipped:", reason)
+        return
+    elif isinstance(exc, AssertionError):
+        test_result.failuresNum += 1
+        test_result.failures.append((current_test, ex_str))
+        if verbose:
+            print(" FAIL")
+    else:
+        test_result.errorsNum += 1
+        test_result.errors.append((current_test, ex_str))
+        if verbose:
+            print(" ERROR")
+    test_result._newFailures += 1
+
+
+def _run_suite(c, test_result: TestResult, suite_name=""):
+    if isinstance(c, TestSuite):
+        c.run(test_result)
+        return
+
+    if isinstance(c, type):
+        o = c()
+    else:
+        o = c
+    set_up_class = getattr(o, "setUpClass", lambda: None)
+    tear_down_class = getattr(o, "tearDownClass", lambda: None)
+    set_up = getattr(o, "setUp", lambda: None)
+    tear_down = getattr(o, "tearDown", lambda: None)
+    exceptions = []
+    try:
+        suite_name += "." + c.__qualname__
+    except AttributeError:
+        pass
+
+    def run_one(test_function):
+        global __test_result__, __current_test__
+        print("%s (%s) ..." % (name, suite_name), end="")
+        set_up()
+        __test_result__ = test_result
+        test_container = f"({suite_name})"
+        __current_test__ = (name, test_container)
+        try:
+            test_result._newFailures = 0
+            test_result.testsRun += 1
+            test_function()
+            # No exception occurred, test passed
+            if test_result._newFailures:
+                print(" FAIL")
+            else:
+                print(" ok")
+        except Exception as ex:
+            _handle_test_exception(
+                current_test=(name, c), test_result=test_result, exc_info=(type(ex), ex, None)
+            )
+            # Uncomment to investigate failure in detail
+            # raise ex
+        finally:
+            __test_result__ = None
+            __current_test__ = None
+            tear_down()
+            try:
+                o.doCleanups()
+            except AttributeError:
+                pass
+
+    set_up_class()
+    try:
+        if hasattr(o, "runTest"):
+            name = str(o)
+            run_one(o.runTest)
+            return
+
+        for name in dir(o):
+            if name.startswith("test"):
+                m = getattr(o, name)
+                if not callable(m):
+                    continue
+                run_one(m)
+
+        if callable(o):
+            name = o.__name__
+            run_one(o)
+    finally:
+        tear_down_class()
+
+    return exceptions
+
+
+# This supports either:
+#
+# >>> import mytest
+# >>> unitttest.main(mytest)
+#
+# >>> unittest.main("mytest")
+#
+# Or, a script that ends with:
+# if __name__ == "__main__":
+#     unittest.main()
+# e.g. run via `mpremote run mytest.py`
+def main(module="__main__", testRunner=None):
+    if testRunner is None:
+        testRunner = TestRunner()
+    elif isinstance(testRunner, type):
+        testRunner = testRunner()
+
+    if isinstance(module, str):
+        module = __import__(module)
+    suite = TestSuite(module.__name__)
+    suite._load_module(module)
+    return testRunner.run(suite)
diff --git a/python-stdlib/uu/manifest.py b/python-stdlib/uu/manifest.py
new file mode 100644
index 000000000..a1f951a8e
--- /dev/null
+++ b/python-stdlib/uu/manifest.py
@@ -0,0 +1,6 @@
+metadata(version="0.5.1")
+
+require("binascii")
+require("os-path")
+
+module("uu.py")
diff --git a/uu/uu.py b/python-stdlib/uu/uu.py
similarity index 75%
rename from uu/uu.py
rename to python-stdlib/uu/uu.py
index d68d29374..03f8b2df1 100644
--- a/uu/uu.py
+++ b/python-stdlib/uu/uu.py
@@ -36,9 +36,11 @@
 
 __all__ = ["Error", "encode", "decode"]
 
+
 class Error(Exception):
     pass
 
+
 def encode(in_file, out_file, name=None, mode=None):
     """Uuencode file"""
     #
@@ -46,7 +48,7 @@ def encode(in_file, out_file, name=None, mode=None):
     #
     opened_files = []
     try:
-        if in_file == '-':
+        if in_file == "-":
             in_file = sys.stdin.buffer
         elif isinstance(in_file, str):
             if name is None:
@@ -56,32 +58,32 @@ def encode(in_file, out_file, name=None, mode=None):
                     mode = os.stat(in_file).st_mode
                 except AttributeError:
                     pass
-            in_file = open(in_file, 'rb')
+            in_file = open(in_file, "rb")
             opened_files.append(in_file)
         #
         # Open out_file if it is a pathname
         #
-        if out_file == '-':
+        if out_file == "-":
             out_file = sys.stdout.buffer
         elif isinstance(out_file, str):
-            out_file = open(out_file, 'wb')
+            out_file = open(out_file, "wb")
             opened_files.append(out_file)
         #
         # Set defaults for name and mode
         #
         if name is None:
-            name = '-'
+            name = "-"
         if mode is None:
             mode = 0o666
         #
         # Write the data
         #
-        out_file.write(('begin %o %s\n' % ((mode & 0o777), name)).encode("ascii"))
+        out_file.write(("begin %o %s\n" % ((mode & 0o777), name)).encode("ascii"))
         data = in_file.read(45)
         while len(data) > 0:
             out_file.write(binascii.b2a_uu(data))
             data = in_file.read(45)
-        out_file.write(b' \nend\n')
+        out_file.write(b" \nend\n")
     finally:
         for f in opened_files:
             f.close()
@@ -93,10 +95,10 @@ def decode(in_file, out_file=None, mode=None, quiet=False):
     # Open the input file, if needed.
     #
     opened_files = []
-    if in_file == '-':
+    if in_file == "-":
         in_file = sys.stdin.buffer
     elif isinstance(in_file, str):
-        in_file = open(in_file, 'rb')
+        in_file = open(in_file, "rb")
         opened_files.append(in_file)
 
     try:
@@ -106,11 +108,11 @@ def decode(in_file, out_file=None, mode=None, quiet=False):
         while True:
             hdr = in_file.readline()
             if not hdr:
-                raise Error('No valid begin line found in input file')
-            if not hdr.startswith(b'begin'):
+                raise Error("No valid begin line found in input file")
+            if not hdr.startswith(b"begin"):
                 continue
-            hdrfields = hdr.split(b' ', 2)
-            if len(hdrfields) == 3 and hdrfields[0] == b'begin':
+            hdrfields = hdr.split(b" ", 2)
+            if len(hdrfields) == 3 and hdrfields[0] == b"begin":
                 try:
                     int(hdrfields[1], 8)
                     break
@@ -118,18 +120,18 @@ def decode(in_file, out_file=None, mode=None, quiet=False):
                     pass
         if out_file is None:
             # If the filename isn't ASCII, what's up with that?!?
-            out_file = hdrfields[2].rstrip(b' \t\r\n\f').decode("ascii")
+            out_file = hdrfields[2].rstrip(b" \t\r\n\f").decode("ascii")
             if os.path.exists(out_file):
-                raise Error('Cannot overwrite existing file: %s' % out_file)
+                raise Error("Cannot overwrite existing file: %s" % out_file)
         if mode is None:
             mode = int(hdrfields[1], 8)
         #
         # Open the output file
         #
-        if out_file == '-':
+        if out_file == "-":
             out_file = sys.stdout.buffer
         elif isinstance(out_file, str):
-            fp = open(out_file, 'wb')
+            fp = open(out_file, "wb")
             try:
                 os.path.chmod(out_file, mode)
             except AttributeError:
@@ -140,34 +142,50 @@ def decode(in_file, out_file=None, mode=None, quiet=False):
         # Main decoding loop
         #
         s = in_file.readline()
-        while s and s.strip(b' \t\r\n\f') != b'end':
+        while s and s.strip(b" \t\r\n\f") != b"end":
             try:
                 data = binascii.a2b_uu(s)
             except binascii.Error as v:
                 # Workaround for broken uuencoders by /Fredrik Lundh
-                nbytes = (((s[0]-32) & 63) * 4 + 5) // 3
+                nbytes = (((s[0] - 32) & 63) * 4 + 5) // 3
                 data = binascii.a2b_uu(s[:nbytes])
                 if not quiet:
                     sys.stderr.write("Warning: %s\n" % v)
             out_file.write(data)
             s = in_file.readline()
         if not s:
-            raise Error('Truncated input file')
+            raise Error("Truncated input file")
     finally:
         for f in opened_files:
             f.close()
 
+
 def test():
     """uuencode/uudecode main program"""
 
     import optparse
-    parser = optparse.OptionParser(usage='usage: %prog [-d] [-t] [input [output]]')
-    parser.add_option('-d', '--decode', dest='decode', help='Decode (instead of encode)?', default=False, action='store_true')
-    parser.add_option('-t', '--text', dest='text', help='data is text, encoded format unix-compatible text?', default=False, action='store_true')
+
+    parser = optparse.OptionParser(usage="usage: %prog [-d] [-t] [input [output]]")
+    parser.add_option(
+        "-d",
+        "--decode",
+        dest="decode",
+        help="Decode (instead of encode)?",
+        default=False,
+        action="store_true",
+    )
+    parser.add_option(
+        "-t",
+        "--text",
+        dest="text",
+        help="data is text, encoded format unix-compatible text?",
+        default=False,
+        action="store_true",
+    )
 
     (options, args) = parser.parse_args()
     if len(args) > 2:
-        parser.error('incorrect number of arguments')
+        parser.error("incorrect number of arguments")
         sys.exit(1)
 
     # Use the binary streams underlying stdin/stdout
@@ -181,19 +199,20 @@ def test():
     if options.decode:
         if options.text:
             if isinstance(output, str):
-                output = open(output, 'wb')
+                output = open(output, "wb")
             else:
-                print(sys.argv[0], ': cannot do -t to stdout')
+                print(sys.argv[0], ": cannot do -t to stdout")
                 sys.exit(1)
         decode(input, output)
     else:
         if options.text:
             if isinstance(input, str):
-                input = open(input, 'rb')
+                input = open(input, "rb")
             else:
-                print(sys.argv[0], ': cannot do -t from stdin')
+                print(sys.argv[0], ": cannot do -t from stdin")
                 sys.exit(1)
         encode(input, output)
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     test()
diff --git a/python-stdlib/venv/manifest.py b/python-stdlib/venv/manifest.py
new file mode 100644
index 000000000..9c36b312f
--- /dev/null
+++ b/python-stdlib/venv/manifest.py
@@ -0,0 +1,9 @@
+metadata(
+    version="0.1.0",
+    description="Support for creating MicroPython virtual environments using `micropython -m venv`",
+)
+
+require("argparse")
+require("mip-cmdline")
+
+package("venv")
diff --git a/python-stdlib/venv/venv/__main__.py b/python-stdlib/venv/venv/__main__.py
new file mode 100644
index 000000000..36ed41dff
--- /dev/null
+++ b/python-stdlib/venv/venv/__main__.py
@@ -0,0 +1,99 @@
+# Support for creating MicroPython virtual environments using `micropython -m venv`
+# MIT license; Copyright (c) 2022 Jim Mussared
+
+import argparse
+import os
+import sys
+
+
+# If mip is not frozen into this binary, then also install it in the venv.
+def install_mip(venv_lib_path):
+    need_mip = False
+    if "mip" in sys.modules:
+        del sys.modules["mip"]
+    saved_sys_path = sys.path[:]
+    try:
+        sys.path[:] = [".frozen"]
+        try:
+            import mip
+
+            print("mip is frozen")
+        except ImportError:
+            need_mip = True
+    finally:
+        sys.path[:] = saved_sys_path
+
+    if need_mip:
+        import mip
+
+        mip.install("mip-cmdline", target=venv_lib_path)
+
+
+def do_venv():
+    parser = argparse.ArgumentParser(description="Create a micropython virtual environment")
+    parser.add_argument("path", nargs=1, help="Path to create the virtual environment in")
+    args = parser.parse_args(args=sys.argv[1:])
+    venv_path = args.path[0]
+    print("Creating virtual environment in:", venv_path)
+
+    # Equivalent to path = os.abspath(path).
+    if not venv_path.startswith("/"):
+        venv_path = os.getcwd() + os.sep + venv_path
+
+    venv_bin_path = venv_path + os.sep + "bin"
+    venv_lib_path = venv_path + os.sep + "lib"
+
+    for d in (
+        venv_path,
+        venv_bin_path,
+        venv_lib_path,
+    ):
+        try:
+            os.mkdir(d)
+        except:
+            pass
+
+    # Note the venv/lib dir goes before .frozen so that installed packages replace frozen ones.
+    with open(venv_bin_path + os.sep + "activate", "w") as f:
+        print(
+            """# Usage: source bin/activate
+
+deactivate() {{
+    PATH="$_OLD_VIRTUAL_PATH"
+    export PATH
+
+    MICROPYPATH="$_OLD_VIRTUAL_MICROPYPATH"
+    if [ -z "$MICROPYPATH" ]; then
+        export -n MICROPYPATH
+    else
+        export MICROPYPATH
+    fi
+
+    unset VIRTUAL_ENV
+
+    unset deactivate
+}}
+
+VIRTUAL_ENV={}
+
+_OLD_VIRTUAL_PATH="$PATH"
+PATH="$VIRTUAL_ENV/bin:$PATH"
+export PATH
+
+_OLD_VIRTUAL_MICROPYPATH="$MICROPYPATH"
+MICROPYPATH="$VIRTUAL_ENV/lib:.frozen"
+export MICROPYPATH
+""".format(
+                venv_path
+            ),
+            file=f,
+        )
+
+    # Add a `micropython` binary in $PATH pointing to this binary.
+    if hasattr(sys, "executable"):
+        os.system("cp {} {}".format(sys.executable, venv_bin_path + os.sep + "micropython"))
+
+    install_mip(venv_lib_path)
+
+
+do_venv()
diff --git a/python-stdlib/warnings/example_warn.py b/python-stdlib/warnings/example_warn.py
new file mode 100644
index 000000000..cba2f1065
--- /dev/null
+++ b/python-stdlib/warnings/example_warn.py
@@ -0,0 +1,4 @@
+import warnings
+
+warnings.warn("block_size of %d seems too small; using our " "default of %d.", RuntimeError, 2)
+#                               RuntimeWarning, 2)
diff --git a/python-stdlib/warnings/manifest.py b/python-stdlib/warnings/manifest.py
new file mode 100644
index 000000000..ca41363aa
--- /dev/null
+++ b/python-stdlib/warnings/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.1")
+
+module("warnings.py")
diff --git a/warnings/warnings.py b/python-stdlib/warnings/warnings.py
similarity index 100%
rename from warnings/warnings.py
rename to python-stdlib/warnings/warnings.py
diff --git a/python-stdlib/zlib/manifest.py b/python-stdlib/zlib/manifest.py
new file mode 100644
index 000000000..f95602f25
--- /dev/null
+++ b/python-stdlib/zlib/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="1.0.0", description="Compression and decompression using the deflate algorithm")
+
+module("zlib.py")
diff --git a/python-stdlib/zlib/zlib.py b/python-stdlib/zlib/zlib.py
new file mode 100644
index 000000000..e6c342ef7
--- /dev/null
+++ b/python-stdlib/zlib/zlib.py
@@ -0,0 +1,39 @@
+# MicroPython zlib module
+# MIT license; Copyright (c) 2023 Jim Mussared
+
+import io, deflate
+
+_MAX_WBITS = const(15)
+
+
+def _decode_wbits(wbits, decompress):
+    if -15 <= wbits <= -5:
+        return (
+            deflate.RAW,
+            -wbits,
+        )
+    elif 5 <= wbits <= 15:
+        return (deflate.ZLIB, wbits)
+    elif decompress and wbits == 0:
+        return (deflate.ZLIB,)
+    elif 21 <= wbits <= 31:
+        return (deflate.GZIP, wbits - 16)
+    elif decompress and 35 <= wbits <= 47:
+        return (deflate.AUTO, wbits - 32)
+    else:
+        raise ValueError("wbits")
+
+
+if hasattr(deflate.DeflateIO, "write"):
+
+    def compress(data, wbits=_MAX_WBITS):
+        f = io.BytesIO()
+        with deflate.DeflateIO(f, *_decode_wbits(wbits, False)) as g:
+            g.write(data)
+        return f.getvalue()
+
+
+def decompress(data, wbits=_MAX_WBITS):
+    f = io.BytesIO(data)
+    with deflate.DeflateIO(f, *_decode_wbits(wbits, True)) as g:
+        return g.read()
diff --git a/queue/metadata.txt b/queue/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/queue/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/queue/queue.py b/queue/queue.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/queue/setup.py b/queue/setup.py
deleted file mode 100644
index c9a3c42f9..000000000
--- a/queue/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-queue',
-      version='0.0.1',
-      description='Dummy queue module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['queue'])
diff --git a/quopri/metadata.txt b/quopri/metadata.txt
deleted file mode 100644
index 6db506312..000000000
--- a/quopri/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=cpython
-type=module
-version=0.5
diff --git a/quopri/quopri.py b/quopri/quopri.py
deleted file mode 100644
index 3d0f0ac07..000000000
--- a/quopri/quopri.py
+++ /dev/null
@@ -1,244 +0,0 @@
-#! /usr/bin/env python3
-
-"""Conversions to/from quoted-printable transport encoding as per RFC 1521."""
-
-# (Dec 1991 version).
-
-__all__ = ["encode", "decode", "encodestring", "decodestring"]
-
-ESCAPE = b'='
-MAXLINESIZE = 76
-HEX = b'0123456789ABCDEF'
-EMPTYSTRING = b''
-
-try:
-    from binascii import a2b_qp, b2a_qp
-except ImportError:
-    a2b_qp = None
-    b2a_qp = None
-
-
-def needsquoting(c, quotetabs, header):
-    """Decide whether a particular byte ordinal needs to be quoted.
-
-    The 'quotetabs' flag indicates whether embedded tabs and spaces should be
-    quoted.  Note that line-ending tabs and spaces are always encoded, as per
-    RFC 1521.
-    """
-    assert isinstance(c, bytes)
-    if c in b' \t':
-        return quotetabs
-    # if header, we have to escape _ because _ is used to escape space
-    if c == b'_':
-        return header
-    return c == ESCAPE or not (b' ' <= c <= b'~')
-
-def quote(c):
-    """Quote a single character."""
-    assert isinstance(c, bytes) and len(c)==1
-    c = ord(c)
-    return ESCAPE + bytes((HEX[c//16], HEX[c%16]))
-
-
-
-def encode(input, output, quotetabs, header=False):
-    """Read 'input', apply quoted-printable encoding, and write to 'output'.
-
-    'input' and 'output' are files with readline() and write() methods.
-    The 'quotetabs' flag indicates whether embedded tabs and spaces should be
-    quoted.  Note that line-ending tabs and spaces are always encoded, as per
-    RFC 1521.
-    The 'header' flag indicates whether we are encoding spaces as _ as per
-    RFC 1522.
-    """
-
-    if b2a_qp is not None:
-        data = input.read()
-        odata = b2a_qp(data, quotetabs=quotetabs, header=header)
-        output.write(odata)
-        return
-
-    def write(s, output=output, lineEnd=b'\n'):
-        # RFC 1521 requires that the line ending in a space or tab must have
-        # that trailing character encoded.
-        if s and s[-1:] in b' \t':
-            output.write(s[:-1] + quote(s[-1:]) + lineEnd)
-        elif s == b'.':
-            output.write(quote(s) + lineEnd)
-        else:
-            output.write(s + lineEnd)
-
-    prevline = None
-    while 1:
-        line = input.readline()
-        if not line:
-            break
-        outline = []
-        # Strip off any readline induced trailing newline
-        stripped = b''
-        if line[-1:] == b'\n':
-            line = line[:-1]
-            stripped = b'\n'
-        # Calculate the un-length-limited encoded line
-        for c in line:
-            c = bytes((c,))
-            if needsquoting(c, quotetabs, header):
-                c = quote(c)
-            if header and c == b' ':
-                outline.append(b'_')
-            else:
-                outline.append(c)
-        # First, write out the previous line
-        if prevline is not None:
-            write(prevline)
-        # Now see if we need any soft line breaks because of RFC-imposed
-        # length limitations.  Then do the thisline->prevline dance.
-        thisline = EMPTYSTRING.join(outline)
-        while len(thisline) > MAXLINESIZE:
-            # Don't forget to include the soft line break `=' sign in the
-            # length calculation!
-            write(thisline[:MAXLINESIZE-1], lineEnd=b'=\n')
-            thisline = thisline[MAXLINESIZE-1:]
-        # Write out the current line
-        prevline = thisline
-    # Write out the last line, without a trailing newline
-    if prevline is not None:
-        write(prevline, lineEnd=stripped)
-
-def encodestring(s, quotetabs=False, header=False):
-    if b2a_qp is not None:
-        return b2a_qp(s, quotetabs=quotetabs, header=header)
-    from io import BytesIO
-    infp = BytesIO(s)
-    outfp = BytesIO()
-    encode(infp, outfp, quotetabs, header)
-    return outfp.getvalue()
-
-
-
-def decode(input, output, header=False):
-    """Read 'input', apply quoted-printable decoding, and write to 'output'.
-    'input' and 'output' are files with readline() and write() methods.
-    If 'header' is true, decode underscore as space (per RFC 1522)."""
-
-    if a2b_qp is not None:
-        data = input.read()
-        odata = a2b_qp(data, header=header)
-        output.write(odata)
-        return
-
-    new = b''
-    while 1:
-        line = input.readline()
-        if not line: break
-        i, n = 0, len(line)
-        if n > 0 and line[n-1:n] == b'\n':
-            partial = 0; n = n-1
-            # Strip trailing whitespace
-            while n > 0 and line[n-1:n] in b" \t\r":
-                n = n-1
-        else:
-            partial = 1
-        while i < n:
-            c = line[i:i+1]
-            if c == b'_' and header:
-                new = new + b' '; i = i+1
-            elif c != ESCAPE:
-                new = new + c; i = i+1
-            elif i+1 == n and not partial:
-                partial = 1; break
-            elif i+1 < n and line[i+1] == ESCAPE:
-                new = new + ESCAPE; i = i+2
-            elif i+2 < n and ishex(line[i+1:i+2]) and ishex(line[i+2:i+3]):
-                new = new + bytes((unhex(line[i+1:i+3]),)); i = i+3
-            else: # Bad escape sequence -- leave it in
-                new = new + c; i = i+1
-        if not partial:
-            output.write(new + b'\n')
-            new = b''
-    if new:
-        output.write(new)
-
-def decodestring(s, header=False):
-    if a2b_qp is not None:
-        return a2b_qp(s, header=header)
-    from io import BytesIO
-    infp = BytesIO(s)
-    outfp = BytesIO()
-    decode(infp, outfp, header=header)
-    return outfp.getvalue()
-
-
-
-# Other helper functions
-def ishex(c):
-    """Return true if the byte ordinal 'c' is a hexadecimal digit in ASCII."""
-    assert isinstance(c, bytes)
-    return b'0' <= c <= b'9' or b'a' <= c <= b'f' or b'A' <= c <= b'F'
-
-def unhex(s):
-    """Get the integer value of a hexadecimal number."""
-    bits = 0
-    for c in s:
-        c = bytes((c,))
-        if b'0' <= c <= b'9':
-            i = ord('0')
-        elif b'a' <= c <= b'f':
-            i = ord('a')-10
-        elif b'A' <= c <= b'F':
-            i = ord(b'A')-10
-        else:
-            assert False, "non-hex digit "+repr(c)
-        bits = bits*16 + (ord(c) - i)
-    return bits
-
-
-
-def main():
-    import sys
-    import getopt
-    try:
-        opts, args = getopt.getopt(sys.argv[1:], 'td')
-    except getopt.error as msg:
-        sys.stdout = sys.stderr
-        print(msg)
-        print("usage: quopri [-t | -d] [file] ...")
-        print("-t: quote tabs")
-        print("-d: decode; default encode")
-        sys.exit(2)
-    deco = 0
-    tabs = 0
-    for o, a in opts:
-        if o == '-t': tabs = 1
-        if o == '-d': deco = 1
-    if tabs and deco:
-        sys.stdout = sys.stderr
-        print("-t and -d are mutually exclusive")
-        sys.exit(2)
-    if not args: args = ['-']
-    sts = 0
-    for file in args:
-        if file == '-':
-            fp = sys.stdin.buffer
-        else:
-            try:
-                fp = open(file, "rb")
-            except IOError as msg:
-                sys.stderr.write("%s: can't open (%s)\n" % (file, msg))
-                sts = 1
-                continue
-        try:
-            if deco:
-                decode(fp, sys.stdout.buffer)
-            else:
-                encode(fp, sys.stdout.buffer, tabs)
-        finally:
-            if file != '-':
-                fp.close()
-    if sts:
-        sys.exit(sts)
-
-
-
-if __name__ == '__main__':
-    main()
diff --git a/quopri/setup.py b/quopri/setup.py
deleted file mode 100644
index 5d7753a3c..000000000
--- a/quopri/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-quopri',
-      version='0.5',
-      description='CPython quopri module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['quopri'])
diff --git a/quopri/test_quopri.py b/quopri/test_quopri.py
deleted file mode 100644
index 0ea8e33cc..000000000
--- a/quopri/test_quopri.py
+++ /dev/null
@@ -1,208 +0,0 @@
-from test import support
-import unittest
-
-import sys, os, io, subprocess
-import quopri
-
-
-
-ENCSAMPLE = b"""\
-Here's a bunch of special=20
-
-=A1=A2=A3=A4=A5=A6=A7=A8=A9
-=AA=AB=AC=AD=AE=AF=B0=B1=B2=B3
-=B4=B5=B6=B7=B8=B9=BA=BB=BC=BD=BE
-=BF=C0=C1=C2=C3=C4=C5=C6
-=C7=C8=C9=CA=CB=CC=CD=CE=CF
-=D0=D1=D2=D3=D4=D5=D6=D7
-=D8=D9=DA=DB=DC=DD=DE=DF
-=E0=E1=E2=E3=E4=E5=E6=E7
-=E8=E9=EA=EB=EC=ED=EE=EF
-=F0=F1=F2=F3=F4=F5=F6=F7
-=F8=F9=FA=FB=FC=FD=FE=FF
-
-characters... have fun!
-"""
-
-# First line ends with a space
-DECSAMPLE = b"Here's a bunch of special \n" + \
-b"""\
-
-\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9
-\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3
-\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe
-\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6
-\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf
-\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7
-\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf
-\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7
-\xe8\xe9\xea\xeb\xec\xed\xee\xef
-\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7
-\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff
-
-characters... have fun!
-"""
-
-
-def withpythonimplementation(testfunc):
-    def newtest(self):
-        # Test default implementation
-        testfunc(self)
-        # Test Python implementation
-        if quopri.b2a_qp is not None or quopri.a2b_qp is not None:
-            oldencode = quopri.b2a_qp
-            olddecode = quopri.a2b_qp
-            try:
-                quopri.b2a_qp = None
-                quopri.a2b_qp = None
-                testfunc(self)
-            finally:
-                quopri.b2a_qp = oldencode
-                quopri.a2b_qp = olddecode
-#    newtest.__name__ = testfunc.__name__
-    return newtest
-
-class QuopriTestCase(unittest.TestCase):
-    # Each entry is a tuple of (plaintext, encoded string).  These strings are
-    # used in the "quotetabs=0" tests.
-    STRINGS = (
-        # Some normal strings
-        (b'hello', b'hello'),
-        (b'''hello
-        there
-        world''', b'''hello
-        there
-        world'''),
-        (b'''hello
-        there
-        world
-''', b'''hello
-        there
-        world
-'''),
-        (b'\201\202\203', b'=81=82=83'),
-        # Add some trailing MUST QUOTE strings
-        (b'hello ', b'hello=20'),
-        (b'hello\t', b'hello=09'),
-        # Some long lines.  First, a single line of 108 characters
-        (b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\xd8\xd9\xda\xdb\xdc\xdd\xde\xdfxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
-         b'''xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=D8=D9=DA=DB=DC=DD=DE=DFx=
-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'''),
-        # A line of exactly 76 characters, no soft line break should be needed
-        (b'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy',
-        b'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'),
-        # A line of 77 characters, forcing a soft line break at position 75,
-        # and a second line of exactly 2 characters (because the soft line
-        # break `=' sign counts against the line length limit).
-        (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz',
-         b'''zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=
-zz'''),
-        # A line of 151 characters, forcing a soft line break at position 75,
-        # with a second line of exactly 76 characters and no trailing =
-        (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz',
-         b'''zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=
-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'''),
-        # A string containing a hard line break, but which the first line is
-        # 151 characters and the second line is exactly 76 characters.  This
-        # should leave us with three lines, the first which has a soft line
-        # break, and which the second and third do not.
-        (b'''yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz''',
-         b'''yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy=
-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'''),
-        # Now some really complex stuff ;)
-        (DECSAMPLE, ENCSAMPLE),
-        )
-
-    # These are used in the "quotetabs=1" tests.
-    ESTRINGS = (
-        (b'hello world', b'hello=20world'),
-        (b'hello\tworld', b'hello=09world'),
-        )
-
-    # These are used in the "header=1" tests.
-    HSTRINGS = (
-        (b'hello world', b'hello_world'),
-        (b'hello_world', b'hello=5Fworld'),
-        )
-
-    @withpythonimplementation
-    def test_encodestring(self):
-        for p, e in self.STRINGS:
-            self.assertEqual(quopri.encodestring(p), e)
-
-    @withpythonimplementation
-    def test_decodestring(self):
-        for p, e in self.STRINGS:
-            self.assertEqual(quopri.decodestring(e), p)
-
-    @withpythonimplementation
-    def test_idempotent_string(self):
-        for p, e in self.STRINGS:
-            self.assertEqual(quopri.decodestring(quopri.encodestring(e)), e)
-
-    @withpythonimplementation
-    def test_encode(self):
-        for p, e in self.STRINGS:
-            infp = io.BytesIO(p)
-            outfp = io.BytesIO()
-            quopri.encode(infp, outfp, quotetabs=False)
-            self.assertEqual(outfp.getvalue(), e)
-
-    @withpythonimplementation
-    def test_decode(self):
-        for p, e in self.STRINGS:
-            infp = io.BytesIO(e)
-            outfp = io.BytesIO()
-            quopri.decode(infp, outfp)
-            self.assertEqual(outfp.getvalue(), p)
-
-    @withpythonimplementation
-    def test_embedded_ws(self):
-        for p, e in self.ESTRINGS:
-            self.assertEqual(quopri.encodestring(p, quotetabs=True), e)
-            self.assertEqual(quopri.decodestring(e), p)
-
-    @withpythonimplementation
-    def test_encode_header(self):
-        for p, e in self.HSTRINGS:
-            self.assertEqual(quopri.encodestring(p, header=True), e)
-
-    @withpythonimplementation
-    def test_decode_header(self):
-        for p, e in self.HSTRINGS:
-            self.assertEqual(quopri.decodestring(e, header=True), p)
-
-    def _test_scriptencode(self):
-        (p, e) = self.STRINGS[-1]
-        process = subprocess.Popen([sys.executable, "-mquopri"],
-                                   stdin=subprocess.PIPE, stdout=subprocess.PIPE)
-        self.addCleanup(process.stdout.close)
-        cout, cerr = process.communicate(p)
-        # On Windows, Python will output the result to stdout using
-        # CRLF, as the mode of stdout is text mode. To compare this
-        # with the expected result, we need to do a line-by-line comparison.
-        cout = cout.decode('latin-1').splitlines()
-        e = e.decode('latin-1').splitlines()
-        assert len(cout)==len(e)
-        for i in range(len(cout)):
-            self.assertEqual(cout[i], e[i])
-        self.assertEqual(cout, e)
-
-    def _test_scriptdecode(self):
-        (p, e) = self.STRINGS[-1]
-        process = subprocess.Popen([sys.executable, "-mquopri", "-d"],
-                                   stdin=subprocess.PIPE, stdout=subprocess.PIPE)
-        self.addCleanup(process.stdout.close)
-        cout, cerr = process.communicate(e)
-        cout = cout.decode('latin-1')
-        p = p.decode('latin-1')
-        self.assertEqual(cout.splitlines(), p.splitlines())
-
-def test_main():
-    support.run_unittest(QuopriTestCase)
-
-
-if __name__ == "__main__":
-    test_main()
diff --git a/random/metadata.txt b/random/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/random/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/random/random.py b/random/random.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/random/setup.py b/random/setup.py
deleted file mode 100644
index a4a0913e3..000000000
--- a/random/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-random',
-      version='0.0.1',
-      description='Dummy random module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['random'])
diff --git a/re-pcre/metadata.txt b/re-pcre/metadata.txt
deleted file mode 100644
index e6b46a056..000000000
--- a/re-pcre/metadata.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-name = re
-srctype = micropython-lib
-type = module
-version = 0.2.1
-author = Paul Sokolovsky
-depends = ffilib
diff --git a/re-pcre/re.py b/re-pcre/re.py
deleted file mode 100644
index f14086544..000000000
--- a/re-pcre/re.py
+++ /dev/null
@@ -1,181 +0,0 @@
-import ffilib
-import array
-
-
-pcre = ffilib.open("libpcre")
-
-#       pcre *pcre_compile(const char *pattern, int options,
-#            const char **errptr, int *erroffset,
-#            const unsigned char *tableptr);
-pcre_compile = pcre.func("p", "pcre_compile", "sipps")
-
-#       int pcre_exec(const pcre *code, const pcre_extra *extra,
-#            const char *subject, int length, int startoffset,
-#            int options, int *ovector, int ovecsize);
-pcre_exec = pcre.func("i", "pcre_exec", "PPsiiipi")
-
-#       int pcre_fullinfo(const pcre *code, const pcre_extra *extra,
-#            int what, void *where);
-pcre_fullinfo = pcre.func("i", "pcre_fullinfo", "PPip")
-
-
-IGNORECASE = I = 1
-MULTILINE = M = 2
-DOTALL = S = 4
-VERBOSE = X = 8
-PCRE_ANCHORED = 0x10
-
-# TODO. Note that Python3 has unicode by default
-ASCII = A = 0
-UNICODE = U = 0
-
-PCRE_INFO_CAPTURECOUNT = 2
-
-
-class PCREMatch:
-
-    def __init__(self, s, num_matches, offsets):
-        self.s = s
-        self.num = num_matches
-        self.offsets = offsets
-
-    def group(self, *n):
-        if len(n) == 1:
-            return self.s[self.offsets[n[0]*2]:self.offsets[n[0]*2+1]]
-        return tuple(self.s[self.offsets[i*2]:self.offsets[i*2+1]] for i in n)
-
-    def groups(self, default=None):
-        assert default is None
-        return tuple(self.group(i + 1) for i in range(self.num - 1))
-
-    def start(self, n=0):
-        return self.offsets[n*2]
-
-    def end(self, n=0):
-        return self.offsets[n*2+1]
-
-    def span(self, n=0):
-        return self.offsets[n*2], self.offsets[n*2+1]
-
-
-class PCREPattern:
-
-    def __init__(self, compiled_ptn):
-        self.obj = compiled_ptn
-
-    def search(self, s, pos=0, endpos=-1, _flags=0):
-        assert endpos == -1, "pos: %d, endpos: %d" % (pos, endpos)
-        buf = bytes(4)
-        pcre_fullinfo(self.obj, None, PCRE_INFO_CAPTURECOUNT, buf)
-        cap_count = int.from_bytes(buf)
-        ov = array.array('i', [0, 0, 0] * (cap_count + 1))
-        num = pcre_exec(self.obj, None, s, len(s), pos, _flags, ov, len(ov))
-        if num == -1:
-            # No match
-            return None
-        # We don't care how many matching subexpressions we got, we
-        # care only about total # of capturing ones (including empty)
-        return PCREMatch(s, cap_count + 1, ov)
-
-    def match(self, s, pos=0, endpos=-1):
-        return self.search(s, pos, endpos, PCRE_ANCHORED)
-
-    def sub(self, repl, s, count=0):
-        if not callable(repl):
-            assert "\\" not in repl, "Backrefs not implemented"
-        res = ""
-        while s:
-            m = self.search(s)
-            if not m:
-                return res + s
-            beg, end = m.span()
-            res += s[:beg]
-            if callable(repl):
-                res += repl(m)
-            else:
-                res += repl
-            s = s[end:]
-            if count != 0:
-                count -= 1
-                if count == 0:
-                    return res + s
-        return res
-
-    def split(self, s, maxsplit=0):
-        res = []
-        while True:
-            m = self.search(s)
-            g = None
-            if m:
-                g = m.group(0)
-            if not m or not g:
-                res.append(s)
-                return res
-            beg, end = m.span(0)
-            res.append(s[:beg])
-            if m.num > 1:
-                res.extend(m.groups())
-            s = s[end:]
-            if maxsplit > 0:
-                maxsplit -= 1
-                if maxsplit == 0:
-                    res.append(s)
-                    return res
-
-    def findall(self, s):
-        res = []
-        start = 0
-        while True:
-            m = self.search(s, start)
-            if not m:
-                return res
-            if m.num == 1:
-                res.append(m.group(0))
-            elif m.num == 2:
-                res.append(m.group(1))
-            else:
-                res.append(m.groups())
-            beg, end = m.span(0)
-            start = end
-
-
-def compile(pattern, flags=0):
-    errptr = bytes(4)
-    erroffset = bytes(4)
-    regex = pcre_compile(pattern, flags, errptr, erroffset, None)
-    assert regex
-    return PCREPattern(regex)
-
-
-def search(pattern, string, flags=0):
-    r = compile(pattern, flags)
-    return r.search(string)
-
-
-def match(pattern, string, flags=0):
-    r = compile(pattern, flags | PCRE_ANCHORED)
-    return r.search(string)
-
-
-def sub(pattern, repl, s, count=0, flags=0):
-    r = compile(pattern, flags)
-    return r.sub(repl, s, count)
-
-
-def split(pattern, s, maxsplit=0, flags=0):
-    r = compile(pattern, flags)
-    return r.split(s, maxsplit)
-
-def findall(pattern, s, flags=0):
-    r = compile(pattern, flags)
-    return r.findall(s)
-
-
-def escape(s):
-    res = ""
-    for c in s:
-        if '0' <= c <= '9' or 'A' <= c <= 'Z' or 'a' <= c <= 'z' or c == '_':
-            res += c
-        else:
-            res += "\\" + c
-    return res
diff --git a/re-pcre/setup.py b/re-pcre/setup.py
deleted file mode 100644
index b81e47416..000000000
--- a/re-pcre/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-re-pcre',
-      version='0.2.1',
-      description='re-pcre module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['re'],
-      install_requires=['micropython-ffilib'])
diff --git a/re-pcre/test_re.py b/re-pcre/test_re.py
deleted file mode 100644
index 9b2a941bb..000000000
--- a/re-pcre/test_re.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import re
-
-m = re.search(r"a+", "caaab")
-assert m.group(0) == "aaa"
-
-m = re.match(r"(?ms)foo.*\Z", "foo\nbar")
-assert m.group(0) == "foo\nbar"
-
-assert re.match(r"a+", "caaab") is None
-m = re.match(r"a+", "aaaab")
-assert m.group(0) == "aaaa"
-
-assert re.sub("a", "z", "caaab") == "czzzb"
-assert re.sub("a+", "z", "caaab") == "czb"
-assert re.sub("a", "z", "caaab", 1) == "czaab"
-assert re.sub("a", "z", "caaab", 2) == "czzab"
-assert re.sub("a", "z", "caaab", 10) == "czzzb"
-
-assert re.sub("a", lambda m: m.group(0) * 2, "caaab") == "caaaaaab"
-
-m = re.match(r"(\d+)\.(\d+)", "24.1632")
-assert m.groups() == ('24', '1632')
-assert m.group(2, 1) == ('1632', '24')
-
-assert re.escape(r"1243*&[]_dsfAd") == r"1243\*\&\[\]_dsfAd"
-
-assert re.split('x*', 'foo') == ['foo']
-assert re.split("(?m)^$", "foo\n\nbar\n") == ["foo\n\nbar\n"]
-assert re.split('\W+', 'Words, words, words.') == ['Words', 'words', 'words', '']
-assert re.split('(\W+)', 'Words, words, words.') == ['Words', ', ', 'words', ', ', 'words', '.', '']
-assert re.split('\W+', 'Words, words, words.', 1) == ['Words', 'words, words.']
-assert re.split('[a-f]+', '0a3B9', flags=re.IGNORECASE) == ['0', '3', '9']
-assert re.split('(\W+)', '...words, words...') == ['', '...', 'words', ', ', 'words', '...', '']
-
-assert re.sub(r"[ :/?&]", "_", "http://foo.ua/bar/?a=1&b=baz/") == "http___foo.ua_bar__a=1_b=baz_"
-
-text = "He was carefully disguised but captured quickly by police."
-assert re.findall(r"\w+ly", text) == ['carefully', 'quickly']
-
-text = "He was carefully disguised but captured quickly by police."
-assert re.findall(r"(\w+)(ly)", text) == [('careful', 'ly'), ('quick', 'ly')]
-
-text = "He was carefully disguised but captured quickly by police."
-assert re.findall(r"(\w+)ly", text) == ['careful', 'quick']
-
-_leading_whitespace_re = re.compile('(^[ \t]*)(?:[^ \t\n])', re.MULTILINE)
-text = "\tfoo\n\tbar"
-indents = _leading_whitespace_re.findall(text)
-assert indents == ['\t', '\t']
-
-text = "  \thello there\n  \t  how are you?"
-indents = _leading_whitespace_re.findall(text)
-assert indents == ['  \t', '  \t  ']
diff --git a/reprlib/metadata.txt b/reprlib/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/reprlib/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/reprlib/reprlib.py b/reprlib/reprlib.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/reprlib/setup.py b/reprlib/setup.py
deleted file mode 100644
index 1e98fdaad..000000000
--- a/reprlib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-reprlib',
-      version='0.0.0',
-      description='Dummy reprlib module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['reprlib'])
diff --git a/runpy/metadata.txt b/runpy/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/runpy/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/runpy/runpy.py b/runpy/runpy.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/runpy/setup.py b/runpy/setup.py
deleted file mode 100644
index 810a869fb..000000000
--- a/runpy/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-runpy',
-      version='0.0.0',
-      description='Dummy runpy module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['runpy'])
diff --git a/sched/metadata.txt b/sched/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/sched/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/sched/sched.py b/sched/sched.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/sched/setup.py b/sched/setup.py
deleted file mode 100644
index 64c086afe..000000000
--- a/sched/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-sched',
-      version='0.0.0',
-      description='Dummy sched module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['sched'])
diff --git a/select/metadata.txt b/select/metadata.txt
deleted file mode 100644
index 6a15f99e3..000000000
--- a/select/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1.6
-author = Paul Sokolovsky
-depends = os, ffilib
diff --git a/select/select.py b/select/select.py
deleted file mode 100644
index e7de109df..000000000
--- a/select/select.py
+++ /dev/null
@@ -1,90 +0,0 @@
-import ffi
-import ustruct as struct
-import os
-import errno
-import ffilib
-
-
-libc = ffilib.libc()
-
-#int epoll_create(int size);
-epoll_create = libc.func("i", "epoll_create", "i")
-#int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-epoll_ctl = libc.func("i", "epoll_ctl", "iiiP")
-#int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-epoll_wait = libc.func("i", "epoll_wait", "ipii")
-
-EPOLLIN = 0x001
-EPOLLPRI = 0x002
-EPOLLOUT = 0x004
-EPOLLERR = 0x008
-EPOLLHUP = 0x010
-EPOLLRDHUP = 0x2000
-EPOLLONESHOT = 1 << 30
-EPOLLET  = 1 << 31
-
-EPOLL_CTL_ADD = 1
-EPOLL_CTL_DEL = 2
-EPOLL_CTL_MOD = 3
-
-# TODO: struct epoll_event's 2nd member is union of uint64_t, etc.
-# On x86, uint64_t is 4-byte aligned, on many other platforms - 8-byte.
-# Until uctypes module can assign native struct offset, use dirty hack
-# below.
-# TODO: Get rid of all this dirtiness, move it on C side
-if ffilib.bitness > 32:
-    # On x86_64, epoll_event is packed struct
-    epoll_event = "<IO"
-elif struct.calcsize("IQ") == 12:
-    epoll_event = "IO"
-else:
-    epoll_event = "QO"
-
-class Epoll:
-
-    def __init__(self, epfd):
-        self.epfd = epfd
-        self.evbuf = struct.pack(epoll_event, 0, None)
-        self.registry = {}
-
-    def register(self, fd, eventmask=EPOLLIN|EPOLLPRI|EPOLLOUT, retval=None):
-        "retval is extension to stdlib, value to use in results from .poll()."
-        if retval is None:
-            retval = fd
-        s = struct.pack(epoll_event, eventmask, retval)
-        r = epoll_ctl(self.epfd, EPOLL_CTL_ADD, fd, s)
-        if r == -1 and os.errno_() == errno.EEXIST:
-            r = epoll_ctl(self.epfd, EPOLL_CTL_MOD, fd, s)
-        os.check_error(r)
-        # We must keep reference to retval, or it may be GCed. And we must
-        # keep mapping from fd to retval to be able to get rid of this retval
-        # reference later.
-        self.registry[fd] = retval
-
-    def unregister(self, fd):
-        # Pass dummy event structure, to workaround kernel bug
-        r = epoll_ctl(self.epfd, EPOLL_CTL_DEL, fd, self.evbuf)
-        os.check_error(r)
-        del self.registry[fd]
-
-    def poll(self, timeout=-1):
-        s = bytearray(self.evbuf)
-        while True:
-            n = epoll_wait(self.epfd, s, 1, timeout)
-            if not os.check_error(n):
-                break
-            # TODO: what about timeout value?
-        res = []
-        if n > 0:
-            vals = struct.unpack(epoll_event, s)
-            res.append((vals[1], vals[0]))
-        return res
-
-    def close(self):
-        os.close(self.epfd)
-
-
-def epoll(sizehint=4):
-    fd = epoll_create(sizehint)
-    os.check_error(fd)
-    return Epoll(fd)
diff --git a/select/setup.py b/select/setup.py
deleted file mode 100644
index 5e2299cbc..000000000
--- a/select/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-select',
-      version='0.1.6',
-      description='select module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['select'],
-      install_requires=['micropython-os', 'micropython-ffilib'])
diff --git a/select/test_epoll.py b/select/test_epoll.py
deleted file mode 100644
index 346fc2af9..000000000
--- a/select/test_epoll.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import select
-import os
-
-
-ep = select.epoll()
-ep.register(0, select.EPOLLIN, (lambda x:x, (0,)))
-res = ep.poll(2000)
-print(res)
-for ev, fd in res:
-    print(os.read(fd, 256))
diff --git a/selectors/metadata.txt b/selectors/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/selectors/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/selectors/selectors.py b/selectors/selectors.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/selectors/setup.py b/selectors/setup.py
deleted file mode 100644
index f5778547e..000000000
--- a/selectors/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-selectors',
-      version='0.0.0',
-      description='Dummy selectors module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['selectors'])
diff --git a/shelve/metadata.txt b/shelve/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/shelve/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/shelve/setup.py b/shelve/setup.py
deleted file mode 100644
index eb9c9c199..000000000
--- a/shelve/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-shelve',
-      version='0.0.0',
-      description='Dummy shelve module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['shelve'])
diff --git a/shelve/shelve.py b/shelve/shelve.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/shlex/metadata.txt b/shlex/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/shlex/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/shlex/setup.py b/shlex/setup.py
deleted file mode 100644
index 26aff7b1a..000000000
--- a/shlex/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-shlex',
-      version='0.0.1',
-      description='Dummy shlex module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['shlex'])
diff --git a/shlex/shlex.py b/shlex/shlex.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/shutil/metadata.txt b/shutil/metadata.txt
deleted file mode 100644
index c6134e1b8..000000000
--- a/shutil/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=micropython-lib
-type=module
-version=0.0.2
diff --git a/shutil/setup.py b/shutil/setup.py
deleted file mode 100644
index 00aba5852..000000000
--- a/shutil/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-shutil',
-      version='0.0.2',
-      description='shutil module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['shutil'])
diff --git a/shutil/shutil.py b/shutil/shutil.py
deleted file mode 100644
index c26ea917d..000000000
--- a/shutil/shutil.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Reimplement, because CPython3.3 impl is rather bloated
-import os
-
-
-def rmtree(top):
-    for path, dirs, files in os.walk(top, False):
-        for f in files:
-            os.unlink(path + "/" + f)
-        os.rmdir(path)
-
-def copyfileobj(src, dest, length=512):
-    if hasattr(src, "readinto"):
-        buf = bytearray(length)
-        while True:
-            sz = src.readinto(buf)
-            if not sz:
-                break
-            if sz == length:
-                dest.write(buf)
-            else:
-                b = memoryview(buf)[:sz]
-                dest.write(b)
-    else:
-        while True:
-            buf = src.read(length)
-            if not buf:
-                break
-            dest.write(buf)
diff --git a/signal/metadata.txt b/signal/metadata.txt
deleted file mode 100644
index e9fda4186..000000000
--- a/signal/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.3.1
-author = Paul Sokolovsky
-depends = ffilib
diff --git a/signal/setup.py b/signal/setup.py
deleted file mode 100644
index 11874e126..000000000
--- a/signal/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-signal',
-      version='0.3.1',
-      description='signal module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['signal'],
-      install_requires=['micropython-ffilib'])
diff --git a/smtplib/metadata.txt b/smtplib/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/smtplib/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/smtplib/setup.py b/smtplib/setup.py
deleted file mode 100644
index a86dd3fa9..000000000
--- a/smtplib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-smtplib',
-      version='0.0.0',
-      description='Dummy smtplib module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['smtplib'])
diff --git a/smtplib/smtplib.py b/smtplib/smtplib.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/socket/metadata.txt b/socket/metadata.txt
deleted file mode 100644
index 69ec8e0e7..000000000
--- a/socket/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.5
-author = Paul Sokolovsky
diff --git a/socket/setup.py b/socket/setup.py
deleted file mode 100644
index 18611a8ad..000000000
--- a/socket/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-socket',
-      version='0.5',
-      description='socket module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['socket'])
diff --git a/socketserver/metadata.txt b/socketserver/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/socketserver/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/socketserver/setup.py b/socketserver/setup.py
deleted file mode 100644
index 3ed2d9580..000000000
--- a/socketserver/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-socketserver',
-      version='0.0.0',
-      description='Dummy socketserver module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['socketserver'])
diff --git a/socketserver/socketserver.py b/socketserver/socketserver.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/sqlite3/metadata.txt b/sqlite3/metadata.txt
deleted file mode 100644
index d0e748b29..000000000
--- a/sqlite3/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.2.2
-author = Paul Sokolovsky
-depends = ffilib
diff --git a/sqlite3/setup.py b/sqlite3/setup.py
deleted file mode 100644
index 14586f811..000000000
--- a/sqlite3/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-sqlite3',
-      version='0.2.2',
-      description='sqlite3 module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['sqlite3'],
-      install_requires=['micropython-ffilib'])
diff --git a/sqlite3/sqlite3.py b/sqlite3/sqlite3.py
deleted file mode 100644
index bcd7c68ba..000000000
--- a/sqlite3/sqlite3.py
+++ /dev/null
@@ -1,140 +0,0 @@
-import ffilib
-
-
-sq3 = ffilib.open("libsqlite3")
-
-sqlite3_open = sq3.func("i", "sqlite3_open", "sp")
-#int sqlite3_close(sqlite3*);
-sqlite3_close = sq3.func("i", "sqlite3_close", "p")
-#int sqlite3_prepare(
-#  sqlite3 *db,            /* Database handle */
-#  const char *zSql,       /* SQL statement, UTF-8 encoded */
-#  int nByte,              /* Maximum length of zSql in bytes. */
-#  sqlite3_stmt **ppStmt,  /* OUT: Statement handle */
-#  const char **pzTail     /* OUT: Pointer to unused portion of zSql */
-#);
-sqlite3_prepare = sq3.func("i", "sqlite3_prepare", "psipp")
-#int sqlite3_finalize(sqlite3_stmt *pStmt);
-sqlite3_finalize = sq3.func("i", "sqlite3_finalize", "p")
-#int sqlite3_step(sqlite3_stmt*);
-sqlite3_step = sq3.func("i", "sqlite3_step", "p")
-#int sqlite3_column_count(sqlite3_stmt *pStmt);
-sqlite3_column_count = sq3.func("i", "sqlite3_column_count", "p")
-#int sqlite3_column_type(sqlite3_stmt*, int iCol);
-sqlite3_column_type = sq3.func("i", "sqlite3_column_type", "pi")
-sqlite3_column_int = sq3.func("i", "sqlite3_column_int", "pi")
-# using "d" return type gives wrong results
-sqlite3_column_double = sq3.func("d", "sqlite3_column_double", "pi")
-sqlite3_column_text = sq3.func("s", "sqlite3_column_text", "pi")
-#sqlite3_int64 sqlite3_last_insert_rowid(sqlite3*);
-# TODO: should return long int
-sqlite3_last_insert_rowid = sq3.func("i", "sqlite3_last_insert_rowid", "p")
-#const char *sqlite3_errmsg(sqlite3*);
-sqlite3_errmsg = sq3.func("s", "sqlite3_errmsg", "p")
-
-# Too recent
-##const char *sqlite3_errstr(int);
-#sqlite3_errstr = sq3.func("s", "sqlite3_errstr", "i")
-
-
-SQLITE_OK         = 0
-SQLITE_ERROR      = 1
-SQLITE_BUSY       = 5
-SQLITE_MISUSE     = 21
-SQLITE_ROW        = 100
-SQLITE_DONE       = 101
-
-SQLITE_INTEGER  = 1
-SQLITE_FLOAT    = 2
-SQLITE_TEXT     = 3
-SQLITE_BLOB     = 4
-SQLITE_NULL     = 5
-
-
-class Error(Exception):
-    pass
-
-
-def check_error(db, s):
-    if s != SQLITE_OK:
-        raise Error(s, sqlite3_errmsg(db))
-
-
-class Connections:
-
-    def __init__(self, h):
-        self.h = h
-
-    def cursor(self):
-        return Cursor(self.h)
-
-    def close(self):
-        s = sqlite3_close(self.h)
-        check_error(self.h, s)
-
-
-class Cursor:
-
-    def __init__(self, h):
-        self.h = h
-        self.stmnt = None
-
-    def execute(self, sql, params=None):
-        if params:
-            params = [quote(v) for v in params]
-            sql = sql % tuple(params)
-        print(sql)
-        b = bytearray(4)
-        s = sqlite3_prepare(self.h, sql, -1, b, None)
-        check_error(self.h, s)
-        self.stmnt = int.from_bytes(b)
-        #print("stmnt", self.stmnt)
-        self.num_cols = sqlite3_column_count(self.stmnt)
-        #print("num_cols", self.num_cols)
-        # If it's not select, actually execute it here
-        # num_cols == 0 for statements which don't return data (=> modify it)
-        if not self.num_cols:
-            v = self.fetchone()
-            assert v is None
-            self.lastrowid = sqlite3_last_insert_rowid(self.h)
-
-    def close(self):
-        s = sqlite3_finalize(self.stmnt)
-        check_error(self.h, s)
-
-    def make_row(self):
-        res = []
-        for i in range(self.num_cols):
-            t = sqlite3_column_type(self.stmnt, i)
-            #print("type", t)
-            if t == SQLITE_INTEGER:
-                res.append(sqlite3_column_int(self.stmnt, i))
-            elif t == SQLITE_FLOAT:
-                res.append(sqlite3_column_double(self.stmnt, i))
-            elif t == SQLITE_TEXT:
-                res.append(sqlite3_column_text(self.stmnt, i))
-            else:
-                raise NotImplementedError
-        return tuple(res)
-
-    def fetchone(self):
-        res = sqlite3_step(self.stmnt)
-        #print("step:", res)
-        if res == SQLITE_DONE:
-            return None
-        if res == SQLITE_ROW:
-            return self.make_row()
-        check_error(self.h, res)
-
-
-def connect(fname):
-    b = bytearray(4)
-    sqlite3_open(fname, b)
-    h = int.from_bytes(b)
-    return Connections(h)
-
-
-def quote(val):
-    if isinstance(val, str):
-        return "'%s'" % val
-    return str(val)
diff --git a/sqlite3/test_sqlite3.py b/sqlite3/test_sqlite3.py
deleted file mode 100644
index 7429e9d0d..000000000
--- a/sqlite3/test_sqlite3.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import sqlite3
-
-
-conn = sqlite3.connect(":memory:")
-
-cur = conn.cursor()
-cur.execute("SELECT 1, 'foo', 3.14159 UNION SELECT 3, 3, 3")
-while True:
-    row = cur.fetchone()
-    if row is None:
-        break
-    print(row)
diff --git a/stat/metadata.txt b/stat/metadata.txt
deleted file mode 100644
index 6db506312..000000000
--- a/stat/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=cpython
-type=module
-version=0.5
diff --git a/stat/setup.py b/stat/setup.py
deleted file mode 100644
index 369f466a6..000000000
--- a/stat/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-stat',
-      version='0.5',
-      description='CPython stat module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['stat'])
diff --git a/stat/stat.py b/stat/stat.py
deleted file mode 100644
index 704adfe2e..000000000
--- a/stat/stat.py
+++ /dev/null
@@ -1,149 +0,0 @@
-"""Constants/functions for interpreting results of os.stat() and os.lstat().
-
-Suggested usage: from stat import *
-"""
-
-# Indices for stat struct members in the tuple returned by os.stat()
-
-ST_MODE  = 0
-ST_INO   = 1
-ST_DEV   = 2
-ST_NLINK = 3
-ST_UID   = 4
-ST_GID   = 5
-ST_SIZE  = 6
-ST_ATIME = 7
-ST_MTIME = 8
-ST_CTIME = 9
-
-# Extract bits from the mode
-
-def S_IMODE(mode):
-    """Return the portion of the file's mode that can be set by
-    os.chmod().
-    """
-    return mode & 0o7777
-
-def S_IFMT(mode):
-    """Return the portion of the file's mode that describes the
-    file type.
-    """
-    return mode & 0o170000
-
-# Constants used as S_IFMT() for various file types
-# (not all are implemented on all systems)
-
-S_IFDIR  = 0o040000  # directory
-S_IFCHR  = 0o020000  # character device
-S_IFBLK  = 0o060000  # block device
-S_IFREG  = 0o100000  # regular file
-S_IFIFO  = 0o010000  # fifo (named pipe)
-S_IFLNK  = 0o120000  # symbolic link
-S_IFSOCK = 0o140000  # socket file
-
-# Functions to test for each file type
-
-def S_ISDIR(mode):
-    """Return True if mode is from a directory."""
-    return S_IFMT(mode) == S_IFDIR
-
-def S_ISCHR(mode):
-    """Return True if mode is from a character special device file."""
-    return S_IFMT(mode) == S_IFCHR
-
-def S_ISBLK(mode):
-    """Return True if mode is from a block special device file."""
-    return S_IFMT(mode) == S_IFBLK
-
-def S_ISREG(mode):
-    """Return True if mode is from a regular file."""
-    return S_IFMT(mode) == S_IFREG
-
-def S_ISFIFO(mode):
-    """Return True if mode is from a FIFO (named pipe)."""
-    return S_IFMT(mode) == S_IFIFO
-
-def S_ISLNK(mode):
-    """Return True if mode is from a symbolic link."""
-    return S_IFMT(mode) == S_IFLNK
-
-def S_ISSOCK(mode):
-    """Return True if mode is from a socket."""
-    return S_IFMT(mode) == S_IFSOCK
-
-# Names for permission bits
-
-S_ISUID = 0o4000  # set UID bit
-S_ISGID = 0o2000  # set GID bit
-S_ENFMT = S_ISGID # file locking enforcement
-S_ISVTX = 0o1000  # sticky bit
-S_IREAD = 0o0400  # Unix V7 synonym for S_IRUSR
-S_IWRITE = 0o0200 # Unix V7 synonym for S_IWUSR
-S_IEXEC = 0o0100  # Unix V7 synonym for S_IXUSR
-S_IRWXU = 0o0700  # mask for owner permissions
-S_IRUSR = 0o0400  # read by owner
-S_IWUSR = 0o0200  # write by owner
-S_IXUSR = 0o0100  # execute by owner
-S_IRWXG = 0o0070  # mask for group permissions
-S_IRGRP = 0o0040  # read by group
-S_IWGRP = 0o0020  # write by group
-S_IXGRP = 0o0010  # execute by group
-S_IRWXO = 0o0007  # mask for others (not in group) permissions
-S_IROTH = 0o0004  # read by others
-S_IWOTH = 0o0002  # write by others
-S_IXOTH = 0o0001  # execute by others
-
-# Names for file flags
-
-UF_NODUMP    = 0x00000001  # do not dump file
-UF_IMMUTABLE = 0x00000002  # file may not be changed
-UF_APPEND    = 0x00000004  # file may only be appended to
-UF_OPAQUE    = 0x00000008  # directory is opaque when viewed through a union stack
-UF_NOUNLINK  = 0x00000010  # file may not be renamed or deleted
-UF_COMPRESSED = 0x00000020 # OS X: file is hfs-compressed
-UF_HIDDEN    = 0x00008000  # OS X: file should not be displayed
-SF_ARCHIVED  = 0x00010000  # file may be archived
-SF_IMMUTABLE = 0x00020000  # file may not be changed
-SF_APPEND    = 0x00040000  # file may only be appended to
-SF_NOUNLINK  = 0x00100000  # file may not be renamed or deleted
-SF_SNAPSHOT  = 0x00200000  # file is a snapshot file
-
-
-_filemode_table = (
-    ((S_IFLNK,         "l"),
-     (S_IFREG,         "-"),
-     (S_IFBLK,         "b"),
-     (S_IFDIR,         "d"),
-     (S_IFCHR,         "c"),
-     (S_IFIFO,         "p")),
-
-    ((S_IRUSR,         "r"),),
-    ((S_IWUSR,         "w"),),
-    ((S_IXUSR|S_ISUID, "s"),
-     (S_ISUID,         "S"),
-     (S_IXUSR,         "x")),
-
-    ((S_IRGRP,         "r"),),
-    ((S_IWGRP,         "w"),),
-    ((S_IXGRP|S_ISGID, "s"),
-     (S_ISGID,         "S"),
-     (S_IXGRP,         "x")),
-
-    ((S_IROTH,         "r"),),
-    ((S_IWOTH,         "w"),),
-    ((S_IXOTH|S_ISVTX, "t"),
-     (S_ISVTX,         "T"),
-     (S_IXOTH,         "x"))
-)
-
-def filemode(mode):
-    """Convert a file's mode to a string of the form '-rwxrwxrwx'."""
-    perm = []
-    for table in _filemode_table:
-        for bit, char in table:
-            if mode & bit == bit:
-                perm.append(char)
-                break
-        else:
-            perm.append("-")
-    return "".join(perm)
diff --git a/statistics/metadata.txt b/statistics/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/statistics/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/statistics/setup.py b/statistics/setup.py
deleted file mode 100644
index b9ad196d7..000000000
--- a/statistics/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-statistics',
-      version='0.0.0',
-      description='Dummy statistics module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['statistics'])
diff --git a/statistics/statistics.py b/statistics/statistics.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/string/metadata.txt b/string/metadata.txt
deleted file mode 100644
index 6b5dc7328..000000000
--- a/string/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=micropython-lib
-type=module
-version=0.1
diff --git a/string/setup.py b/string/setup.py
deleted file mode 100644
index a8cd9327c..000000000
--- a/string/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-string',
-      version='0.1',
-      description='string module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['string'])
diff --git a/string/string.py b/string/string.py
deleted file mode 100644
index d8ed89e32..000000000
--- a/string/string.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Some strings for ctype-style character classification
-whitespace = ' \t\n\r\v\f'
-ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
-ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
-ascii_letters = ascii_lowercase + ascii_uppercase
-digits = '0123456789'
-hexdigits = digits + 'abcdef' + 'ABCDEF'
-octdigits = '01234567'
-punctuation = """!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
-printable = digits + ascii_letters + punctuation + whitespace
-
-
-def translate(s, map):
-    import io
-    sb = io.StringIO()
-    for c in s:
-        v = ord(c)
-        if v in map:
-            v = map[v]
-            if isinstance(v, int):
-                sb.write(chr(v))
-            elif v is not None:
-                sb.write(v)
-        else:
-            sb.write(c)
-    return sb.getvalue()
diff --git a/stringprep/metadata.txt b/stringprep/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/stringprep/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/stringprep/setup.py b/stringprep/setup.py
deleted file mode 100644
index 1536e1056..000000000
--- a/stringprep/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-stringprep',
-      version='0.0.0',
-      description='Dummy stringprep module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['stringprep'])
diff --git a/stringprep/stringprep.py b/stringprep/stringprep.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/struct/metadata.txt b/struct/metadata.txt
deleted file mode 100644
index a9a85a5bf..000000000
--- a/struct/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1
diff --git a/struct/setup.py b/struct/setup.py
deleted file mode 100644
index 21b745aae..000000000
--- a/struct/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-struct',
-      version='0.1',
-      description='struct module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['struct'])
diff --git a/subprocess/metadata.txt b/subprocess/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/subprocess/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/subprocess/setup.py b/subprocess/setup.py
deleted file mode 100644
index 22a49a364..000000000
--- a/subprocess/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-subprocess',
-      version='0.0.1',
-      description='Dummy subprocess module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['subprocess'])
diff --git a/subprocess/subprocess.py b/subprocess/subprocess.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tarfile/metadata.txt b/tarfile/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/tarfile/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/tarfile/setup.py b/tarfile/setup.py
deleted file mode 100644
index 0a2295d70..000000000
--- a/tarfile/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-tarfile',
-      version='0.0.0',
-      description='Dummy tarfile module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['tarfile'])
diff --git a/tarfile/tarfile.py b/tarfile/tarfile.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/telnetlib/metadata.txt b/telnetlib/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/telnetlib/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/telnetlib/setup.py b/telnetlib/setup.py
deleted file mode 100644
index 79bdabb2c..000000000
--- a/telnetlib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-telnetlib',
-      version='0.0.0',
-      description='Dummy telnetlib module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['telnetlib'])
diff --git a/telnetlib/telnetlib.py b/telnetlib/telnetlib.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tempfile/metadata.txt b/tempfile/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/tempfile/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/tempfile/setup.py b/tempfile/setup.py
deleted file mode 100644
index 7fefb7c24..000000000
--- a/tempfile/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-tempfile',
-      version='0.0.1',
-      description='Dummy tempfile module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['tempfile'])
diff --git a/tempfile/tempfile.py b/tempfile/tempfile.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/test.pystone/metadata.txt b/test.pystone/metadata.txt
deleted file mode 100644
index 8b7e95e4d..000000000
--- a/test.pystone/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = cpython
-type = package
-version = 1.0
diff --git a/test.pystone/setup.py b/test.pystone/setup.py
deleted file mode 100644
index 6084663f4..000000000
--- a/test.pystone/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-test.pystone',
-      version='1.0',
-      description='CPython test.pystone module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['test'])
diff --git a/test.pystone/test/pystone.py b/test.pystone/test/pystone.py
deleted file mode 100755
index a41f1e53a..000000000
--- a/test.pystone/test/pystone.py
+++ /dev/null
@@ -1,277 +0,0 @@
-#! /usr/bin/env python3
-
-"""
-"PYSTONE" Benchmark Program
-
-Version:        Python/1.2 (corresponds to C/1.1 plus 3 Pystone fixes)
-
-Author:         Reinhold P. Weicker,  CACM Vol 27, No 10, 10/84 pg. 1013.
-
-                Translated from ADA to C by Rick Richardson.
-                Every method to preserve ADA-likeness has been used,
-                at the expense of C-ness.
-
-                Translated from C to Python by Guido van Rossum.
-
-Version History:
-
-                Version 1.1 corrects two bugs in version 1.0:
-
-                First, it leaked memory: in Proc1(), NextRecord ends
-                up having a pointer to itself.  I have corrected this
-                by zapping NextRecord.PtrComp at the end of Proc1().
-
-                Second, Proc3() used the operator != to compare a
-                record to None.  This is rather inefficient and not
-                true to the intention of the original benchmark (where
-                a pointer comparison to None is intended; the !=
-                operator attempts to find a method __cmp__ to do value
-                comparison of the record).  Version 1.1 runs 5-10
-                percent faster than version 1.0, so benchmark figures
-                of different versions can't be compared directly.
-
-                Version 1.2 changes the division to floor division.
-
-                Under Python 3 version 1.1 would use the normal division
-                operator, resulting in some of the operations mistakenly
-                yielding floats. Version 1.2 instead uses floor division
-                making the benchmark a integer benchmark again.
-
-"""
-
-LOOPS = 50000
-
-from time import clock
-
-__version__ = "1.2"
-
-[Ident1, Ident2, Ident3, Ident4, Ident5] = range(1, 6)
-
-class Record:
-
-    def __init__(self, PtrComp = None, Discr = 0, EnumComp = 0,
-                       IntComp = 0, StringComp = 0):
-        self.PtrComp = PtrComp
-        self.Discr = Discr
-        self.EnumComp = EnumComp
-        self.IntComp = IntComp
-        self.StringComp = StringComp
-
-    def copy(self):
-        return Record(self.PtrComp, self.Discr, self.EnumComp,
-                      self.IntComp, self.StringComp)
-
-TRUE = 1
-FALSE = 0
-
-def main(loops=LOOPS):
-    benchtime, stones = pystones(loops)
-    print("Pystone(%s) time for %d passes = %g" % \
-          (__version__, loops, benchtime))
-    print("This machine benchmarks at %g pystones/second" % stones)
-
-
-def pystones(loops=LOOPS):
-    return Proc0(loops)
-
-IntGlob = 0
-BoolGlob = FALSE
-Char1Glob = '\0'
-Char2Glob = '\0'
-Array1Glob = [0]*51
-Array2Glob = [x[:] for x in [Array1Glob]*51]
-PtrGlb = None
-PtrGlbNext = None
-
-def Proc0(loops=LOOPS):
-    global IntGlob
-    global BoolGlob
-    global Char1Glob
-    global Char2Glob
-    global Array1Glob
-    global Array2Glob
-    global PtrGlb
-    global PtrGlbNext
-
-    starttime = clock()
-    for i in range(loops):
-        pass
-    nulltime = clock() - starttime
-
-    PtrGlbNext = Record()
-    PtrGlb = Record()
-    PtrGlb.PtrComp = PtrGlbNext
-    PtrGlb.Discr = Ident1
-    PtrGlb.EnumComp = Ident3
-    PtrGlb.IntComp = 40
-    PtrGlb.StringComp = "DHRYSTONE PROGRAM, SOME STRING"
-    String1Loc = "DHRYSTONE PROGRAM, 1'ST STRING"
-    Array2Glob[8][7] = 10
-
-    starttime = clock()
-
-    for i in range(loops):
-        Proc5()
-        Proc4()
-        IntLoc1 = 2
-        IntLoc2 = 3
-        String2Loc = "DHRYSTONE PROGRAM, 2'ND STRING"
-        EnumLoc = Ident2
-        BoolGlob = not Func2(String1Loc, String2Loc)
-        while IntLoc1 < IntLoc2:
-            IntLoc3 = 5 * IntLoc1 - IntLoc2
-            IntLoc3 = Proc7(IntLoc1, IntLoc2)
-            IntLoc1 = IntLoc1 + 1
-        Proc8(Array1Glob, Array2Glob, IntLoc1, IntLoc3)
-        PtrGlb = Proc1(PtrGlb)
-        CharIndex = 'A'
-        while CharIndex <= Char2Glob:
-            if EnumLoc == Func1(CharIndex, 'C'):
-                EnumLoc = Proc6(Ident1)
-            CharIndex = chr(ord(CharIndex)+1)
-        IntLoc3 = IntLoc2 * IntLoc1
-        IntLoc2 = IntLoc3 // IntLoc1
-        IntLoc2 = 7 * (IntLoc3 - IntLoc2) - IntLoc1
-        IntLoc1 = Proc2(IntLoc1)
-
-    benchtime = clock() - starttime - nulltime
-    if benchtime == 0.0:
-        loopsPerBenchtime = 0.0
-    else:
-        loopsPerBenchtime = (loops / benchtime)
-    return benchtime, loopsPerBenchtime
-
-def Proc1(PtrParIn):
-    PtrParIn.PtrComp = NextRecord = PtrGlb.copy()
-    PtrParIn.IntComp = 5
-    NextRecord.IntComp = PtrParIn.IntComp
-    NextRecord.PtrComp = PtrParIn.PtrComp
-    NextRecord.PtrComp = Proc3(NextRecord.PtrComp)
-    if NextRecord.Discr == Ident1:
-        NextRecord.IntComp = 6
-        NextRecord.EnumComp = Proc6(PtrParIn.EnumComp)
-        NextRecord.PtrComp = PtrGlb.PtrComp
-        NextRecord.IntComp = Proc7(NextRecord.IntComp, 10)
-    else:
-        PtrParIn = NextRecord.copy()
-    NextRecord.PtrComp = None
-    return PtrParIn
-
-def Proc2(IntParIO):
-    IntLoc = IntParIO + 10
-    while 1:
-        if Char1Glob == 'A':
-            IntLoc = IntLoc - 1
-            IntParIO = IntLoc - IntGlob
-            EnumLoc = Ident1
-        if EnumLoc == Ident1:
-            break
-    return IntParIO
-
-def Proc3(PtrParOut):
-    global IntGlob
-
-    if PtrGlb is not None:
-        PtrParOut = PtrGlb.PtrComp
-    else:
-        IntGlob = 100
-    PtrGlb.IntComp = Proc7(10, IntGlob)
-    return PtrParOut
-
-def Proc4():
-    global Char2Glob
-
-    BoolLoc = Char1Glob == 'A'
-    BoolLoc = BoolLoc or BoolGlob
-    Char2Glob = 'B'
-
-def Proc5():
-    global Char1Glob
-    global BoolGlob
-
-    Char1Glob = 'A'
-    BoolGlob = FALSE
-
-def Proc6(EnumParIn):
-    EnumParOut = EnumParIn
-    if not Func3(EnumParIn):
-        EnumParOut = Ident4
-    if EnumParIn == Ident1:
-        EnumParOut = Ident1
-    elif EnumParIn == Ident2:
-        if IntGlob > 100:
-            EnumParOut = Ident1
-        else:
-            EnumParOut = Ident4
-    elif EnumParIn == Ident3:
-        EnumParOut = Ident2
-    elif EnumParIn == Ident4:
-        pass
-    elif EnumParIn == Ident5:
-        EnumParOut = Ident3
-    return EnumParOut
-
-def Proc7(IntParI1, IntParI2):
-    IntLoc = IntParI1 + 2
-    IntParOut = IntParI2 + IntLoc
-    return IntParOut
-
-def Proc8(Array1Par, Array2Par, IntParI1, IntParI2):
-    global IntGlob
-
-    IntLoc = IntParI1 + 5
-    Array1Par[IntLoc] = IntParI2
-    Array1Par[IntLoc+1] = Array1Par[IntLoc]
-    Array1Par[IntLoc+30] = IntLoc
-    for IntIndex in range(IntLoc, IntLoc+2):
-        Array2Par[IntLoc][IntIndex] = IntLoc
-    Array2Par[IntLoc][IntLoc-1] = Array2Par[IntLoc][IntLoc-1] + 1
-    Array2Par[IntLoc+20][IntLoc] = Array1Par[IntLoc]
-    IntGlob = 5
-
-def Func1(CharPar1, CharPar2):
-    CharLoc1 = CharPar1
-    CharLoc2 = CharLoc1
-    if CharLoc2 != CharPar2:
-        return Ident1
-    else:
-        return Ident2
-
-def Func2(StrParI1, StrParI2):
-    IntLoc = 1
-    while IntLoc <= 1:
-        if Func1(StrParI1[IntLoc], StrParI2[IntLoc+1]) == Ident1:
-            CharLoc = 'A'
-            IntLoc = IntLoc + 1
-    if CharLoc >= 'W' and CharLoc <= 'Z':
-        IntLoc = 7
-    if CharLoc == 'X':
-        return TRUE
-    else:
-        if StrParI1 > StrParI2:
-            IntLoc = IntLoc + 7
-            return TRUE
-        else:
-            return FALSE
-
-def Func3(EnumParIn):
-    EnumLoc = EnumParIn
-    if EnumLoc == Ident3: return TRUE
-    return FALSE
-
-if __name__ == '__main__':
-    import sys
-    def error(msg):
-        print(msg, end=' ', file=sys.stderr)
-        print("usage: %s [number_of_loops]" % sys.argv[0], file=sys.stderr)
-        sys.exit(100)
-    nargs = len(sys.argv) - 1
-    if nargs > 1:
-        error("%d arguments are too many;" % nargs)
-    elif nargs == 1:
-        try: loops = int(sys.argv[1])
-        except ValueError:
-            error("Invalid argument %r;" % sys.argv[1])
-    else:
-        loops = LOOPS
-    main(loops)
diff --git a/test.support/metadata.txt b/test.support/metadata.txt
deleted file mode 100644
index 8d4724126..000000000
--- a/test.support/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=micropython-lib
-type=package
-version=0.1
diff --git a/test.support/setup.py b/test.support/setup.py
deleted file mode 100644
index 0d9c4c11f..000000000
--- a/test.support/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-test.support',
-      version='0.1',
-      description='test.support module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['test'])
diff --git a/test.support/test/support.py b/test.support/test/support.py
deleted file mode 100644
index cb9cd6f22..000000000
--- a/test.support/test/support.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import sys
-import io
-import unittest
-import gc
-import contextlib
-
-
-TESTFN = '@test'
-
-def run_unittest(*classes):
-    suite = unittest.TestSuite()
-    for c in classes:
-        suite.addTest(c)
-    runner = unittest.TestRunner()
-    result = runner.run(suite)
-    msg = "Ran %d tests" % result.testsRun
-    if result.skippedNum > 0:
-        msg += " (%d skipped)" % result.skippedNum
-    print(msg)
-
-def can_symlink():
-    return False
-
-def skip_unless_symlink(test):
-    """Skip decorator for tests that require functional symlink"""
-    ok = can_symlink()
-    msg = "Requires functional symlink implementation"
-    return test if ok else unittest.skip(msg)(test)
-
-def create_empty_file(name):
-    open(name, "w").close()
-
-@contextlib.contextmanager
-def disable_gc():
-    have_gc = gc.isenabled()
-    gc.disable()
-    try:
-        yield
-    finally:
-        if have_gc:
-            gc.enable()
-
-def gc_collect():
-    gc.collect()
-    gc.collect()
-    gc.collect()
-
-@contextlib.contextmanager
-def captured_output(stream_name):
-    org = getattr(sys, stream_name)
-    buf = io.StringIO()
-    setattr(sys, stream_name, buf)
-    try:
-        yield buf
-    finally:
-        setattr(sys, stream_name, org)
-
-def captured_stderr():
-    return captured_output("stderr")
diff --git a/test/test_pep380.py b/test/test_pep380.py
deleted file mode 100644
index 11d659496..000000000
--- a/test/test_pep380.py
+++ /dev/null
@@ -1,1030 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-Test suite for PEP 380 implementation
-
-adapted from original tests written by Greg Ewing
-see <http://www.cosc.canterbury.ac.nz/greg.ewing/python/yield-from/YieldFrom-Python3.1.2-rev5.zip>
-"""
-
-import unittest
-import io
-import sys
-import inspect
-#import parser
-
-from test.support import captured_stderr, disable_gc, gc_collect
-
-class TestPEP380Operation(unittest.TestCase):
-    """
-    Test semantics.
-    """
-
-    def test_delegation_of_initial_next_to_subgenerator(self):
-        """
-        Test delegation of initial next() call to subgenerator
-        """
-        trace = []
-        def g1():
-            trace.append("Starting g1")
-            yield from g2()
-            trace.append("Finishing g1")
-        def g2():
-            trace.append("Starting g2")
-            yield 42
-            trace.append("Finishing g2")
-        for x in g1():
-            trace.append("Yielded %s" % (x,))
-        self.assertEqual(trace,[
-            "Starting g1",
-            "Starting g2",
-            "Yielded 42",
-            "Finishing g2",
-            "Finishing g1",
-        ])
-
-    def test_raising_exception_in_initial_next_call(self):
-        """
-        Test raising exception in initial next() call
-        """
-        trace = []
-        def g1():
-            try:
-                trace.append("Starting g1")
-                yield from g2()
-            finally:
-                trace.append("Finishing g1")
-        def g2():
-            try:
-                trace.append("Starting g2")
-                raise ValueError("spanish inquisition occurred")
-            finally:
-                trace.append("Finishing g2")
-        try:
-            for x in g1():
-                trace.append("Yielded %s" % (x,))
-        except ValueError as e:
-            self.assertEqual(e.args[0], "spanish inquisition occurred")
-        else:
-            self.fail("subgenerator failed to raise ValueError")
-        self.assertEqual(trace,[
-            "Starting g1",
-            "Starting g2",
-            "Finishing g2",
-            "Finishing g1",
-        ])
-
-    def test_delegation_of_next_call_to_subgenerator(self):
-        """
-        Test delegation of next() call to subgenerator
-        """
-        trace = []
-        def g1():
-            trace.append("Starting g1")
-            yield "g1 ham"
-            yield from g2()
-            yield "g1 eggs"
-            trace.append("Finishing g1")
-        def g2():
-            trace.append("Starting g2")
-            yield "g2 spam"
-            yield "g2 more spam"
-            trace.append("Finishing g2")
-        for x in g1():
-            trace.append("Yielded %s" % (x,))
-        self.assertEqual(trace,[
-            "Starting g1",
-            "Yielded g1 ham",
-            "Starting g2",
-            "Yielded g2 spam",
-            "Yielded g2 more spam",
-            "Finishing g2",
-            "Yielded g1 eggs",
-            "Finishing g1",
-        ])
-
-    def test_raising_exception_in_delegated_next_call(self):
-        """
-        Test raising exception in delegated next() call
-        """
-        trace = []
-        def g1():
-            try:
-                trace.append("Starting g1")
-                yield "g1 ham"
-                yield from g2()
-                yield "g1 eggs"
-            finally:
-                trace.append("Finishing g1")
-        def g2():
-            try:
-                trace.append("Starting g2")
-                yield "g2 spam"
-                raise ValueError("hovercraft is full of eels")
-                yield "g2 more spam"
-            finally:
-                trace.append("Finishing g2")
-        try:
-            for x in g1():
-                trace.append("Yielded %s" % (x,))
-        except ValueError as e:
-            self.assertEqual(e.args[0], "hovercraft is full of eels")
-        else:
-            self.fail("subgenerator failed to raise ValueError")
-        self.assertEqual(trace,[
-            "Starting g1",
-            "Yielded g1 ham",
-            "Starting g2",
-            "Yielded g2 spam",
-            "Finishing g2",
-            "Finishing g1",
-        ])
-
-    def test_delegation_of_send(self):
-        """
-        Test delegation of send()
-        """
-        trace = []
-        def g1():
-            trace.append("Starting g1")
-            x = yield "g1 ham"
-            trace.append("g1 received %s" % (x,))
-            yield from g2()
-            x = yield "g1 eggs"
-            trace.append("g1 received %s" % (x,))
-            trace.append("Finishing g1")
-        def g2():
-            trace.append("Starting g2")
-            x = yield "g2 spam"
-            trace.append("g2 received %s" % (x,))
-            x = yield "g2 more spam"
-            trace.append("g2 received %s" % (x,))
-            trace.append("Finishing g2")
-        g = g1()
-        y = next(g)
-        x = 1
-        try:
-            while 1:
-                y = g.send(x)
-                trace.append("Yielded %s" % (y,))
-                x += 1
-        except StopIteration:
-            pass
-        self.assertEqual(trace,[
-            "Starting g1",
-            "g1 received 1",
-            "Starting g2",
-            "Yielded g2 spam",
-            "g2 received 2",
-            "Yielded g2 more spam",
-            "g2 received 3",
-            "Finishing g2",
-            "Yielded g1 eggs",
-            "g1 received 4",
-            "Finishing g1",
-        ])
-
-    def test_handling_exception_while_delegating_send(self):
-        """
-        Test handling exception while delegating 'send'
-        """
-        trace = []
-        def g1():
-            trace.append("Starting g1")
-            x = yield "g1 ham"
-            trace.append("g1 received %s" % (x,))
-            yield from g2()
-            x = yield "g1 eggs"
-            trace.append("g1 received %s" % (x,))
-            trace.append("Finishing g1")
-        def g2():
-            trace.append("Starting g2")
-            x = yield "g2 spam"
-            trace.append("g2 received %s" % (x,))
-            raise ValueError("hovercraft is full of eels")
-            x = yield "g2 more spam"
-            trace.append("g2 received %s" % (x,))
-            trace.append("Finishing g2")
-        def run():
-            g = g1()
-            y = next(g)
-            x = 1
-            try:
-                while 1:
-                    y = g.send(x)
-                    trace.append("Yielded %s" % (y,))
-                    x += 1
-            except StopIteration:
-                trace.append("StopIteration")
-        self.assertRaises(ValueError,run)
-        self.assertEqual(trace,[
-            "Starting g1",
-            "g1 received 1",
-            "Starting g2",
-            "Yielded g2 spam",
-            "g2 received 2",
-        ])
-
-    def test_delegating_close(self):
-        """
-        Test delegating 'close'
-        """
-        trace = []
-        def g1():
-            try:
-                trace.append("Starting g1")
-                yield "g1 ham"
-                yield from g2()
-                yield "g1 eggs"
-            finally:
-                trace.append("Finishing g1")
-        def g2():
-            try:
-                trace.append("Starting g2")
-                yield "g2 spam"
-                yield "g2 more spam"
-            finally:
-                trace.append("Finishing g2")
-        g = g1()
-        for i in range(2):
-            x = next(g)
-            trace.append("Yielded %s" % (x,))
-        g.close()
-        self.assertEqual(trace,[
-            "Starting g1",
-            "Yielded g1 ham",
-            "Starting g2",
-            "Yielded g2 spam",
-            "Finishing g2",
-            "Finishing g1"
-        ])
-
-    def test_handing_exception_while_delegating_close(self):
-        """
-        Test handling exception while delegating 'close'
-        """
-        trace = []
-        def g1():
-            try:
-                trace.append("Starting g1")
-                yield "g1 ham"
-                yield from g2()
-                yield "g1 eggs"
-            finally:
-                trace.append("Finishing g1")
-        def g2():
-            try:
-                trace.append("Starting g2")
-                yield "g2 spam"
-                yield "g2 more spam"
-            finally:
-                trace.append("Finishing g2")
-                raise ValueError("nybbles have exploded with delight")
-        try:
-            g = g1()
-            for i in range(2):
-                x = next(g)
-                trace.append("Yielded %s" % (x,))
-            g.close()
-        except ValueError as e:
-            self.assertEqual(e.args[0], "nybbles have exploded with delight")
-# MicroPython doesn't support nested exceptions
-#            self.assertIsInstance(e.__context__, GeneratorExit)
-        else:
-            self.fail("subgenerator failed to raise ValueError")
-        self.assertEqual(trace,[
-            "Starting g1",
-            "Yielded g1 ham",
-            "Starting g2",
-            "Yielded g2 spam",
-            "Finishing g2",
-            "Finishing g1",
-        ])
-
-    def test_delegating_throw(self):
-        """
-        Test delegating 'throw'
-        """
-        trace = []
-        def g1():
-            try:
-                trace.append("Starting g1")
-                yield "g1 ham"
-                yield from g2()
-                yield "g1 eggs"
-            finally:
-                trace.append("Finishing g1")
-        def g2():
-            try:
-                trace.append("Starting g2")
-                yield "g2 spam"
-                yield "g2 more spam"
-            finally:
-                trace.append("Finishing g2")
-        try:
-            g = g1()
-            for i in range(2):
-                x = next(g)
-                trace.append("Yielded %s" % (x,))
-            e = ValueError("tomato ejected")
-            g.throw(e)
-        except ValueError as e:
-            self.assertEqual(e.args[0], "tomato ejected")
-        else:
-            self.fail("subgenerator failed to raise ValueError")
-        self.assertEqual(trace,[
-            "Starting g1",
-            "Yielded g1 ham",
-            "Starting g2",
-            "Yielded g2 spam",
-            "Finishing g2",
-            "Finishing g1",
-        ])
-
-    def test_value_attribute_of_StopIteration_exception(self):
-        """
-        Test 'value' attribute of StopIteration exception
-        """
-        trace = []
-        def pex(e):
-            trace.append("%s: %s" % (e.__class__.__name__, e))
-            trace.append("value = %s" % (e.value,))
-        e = StopIteration()
-        pex(e)
-        e = StopIteration("spam")
-        pex(e)
-# MicroPython doesn't support assignment to .value
-#        e.value = "eggs"
-#        pex(e)
-        self.assertEqual(trace,[
-            "StopIteration: ",
-            "value = None",
-            "StopIteration: spam",
-            "value = spam",
-#            "StopIteration: spam",
-#            "value = eggs",
-        ])
-
-
-    def test_exception_value_crash(self):
-        # There used to be a refcount error when the return value
-        # stored in the StopIteration has a refcount of 1.
-        def g1():
-            yield from g2()
-        def g2():
-            yield "g2"
-            return [42]
-        self.assertEqual(list(g1()), ["g2"])
-
-
-    def test_generator_return_value(self):
-        """
-        Test generator return value
-        """
-        trace = []
-        def g1():
-            trace.append("Starting g1")
-            yield "g1 ham"
-            ret = yield from g2()
-            trace.append("g2 returned %s" % (ret,))
-            ret = yield from g2(42)
-            trace.append("g2 returned %s" % (ret,))
-            yield "g1 eggs"
-            trace.append("Finishing g1")
-        def g2(v = None):
-            trace.append("Starting g2")
-            yield "g2 spam"
-            yield "g2 more spam"
-            trace.append("Finishing g2")
-            if v:
-                return v
-        for x in g1():
-            trace.append("Yielded %s" % (x,))
-        self.assertEqual(trace,[
-            "Starting g1",
-            "Yielded g1 ham",
-            "Starting g2",
-            "Yielded g2 spam",
-            "Yielded g2 more spam",
-            "Finishing g2",
-            "g2 returned None",
-            "Starting g2",
-            "Yielded g2 spam",
-            "Yielded g2 more spam",
-            "Finishing g2",
-            "g2 returned 42",
-            "Yielded g1 eggs",
-            "Finishing g1",
-        ])
-
-    def test_delegation_of_next_to_non_generator(self):
-        """
-        Test delegation of next() to non-generator
-        """
-        trace = []
-        def g():
-            yield from range(3)
-        for x in g():
-            trace.append("Yielded %s" % (x,))
-        self.assertEqual(trace,[
-            "Yielded 0",
-            "Yielded 1",
-            "Yielded 2",
-        ])
-
-
-    def test_conversion_of_sendNone_to_next(self):
-        """
-        Test conversion of send(None) to next()
-        """
-        trace = []
-        def g():
-            yield from range(3)
-        gi = g()
-        for x in range(3):
-            y = gi.send(None)
-            trace.append("Yielded: %s" % (y,))
-        self.assertEqual(trace,[
-            "Yielded: 0",
-            "Yielded: 1",
-            "Yielded: 2",
-        ])
-
-    def test_delegation_of_close_to_non_generator(self):
-        """
-        Test delegation of close() to non-generator
-        """
-        trace = []
-        def g():
-            try:
-                trace.append("starting g")
-                yield from range(3)
-                trace.append("g should not be here")
-            finally:
-                trace.append("finishing g")
-        gi = g()
-        next(gi)
-        with captured_stderr() as output:
-            gi.close()
-        self.assertEqual(output.getvalue(), '')
-        self.assertEqual(trace,[
-            "starting g",
-            "finishing g",
-        ])
-
-    def test_delegating_throw_to_non_generator(self):
-        """
-        Test delegating 'throw' to non-generator
-        """
-        trace = []
-        def g():
-            try:
-                trace.append("Starting g")
-                yield from range(10)
-            finally:
-                trace.append("Finishing g")
-        try:
-            gi = g()
-            for i in range(5):
-                x = next(gi)
-                trace.append("Yielded %s" % (x,))
-            e = ValueError("tomato ejected")
-            gi.throw(e)
-        except ValueError as e:
-            self.assertEqual(e.args[0],"tomato ejected")
-        else:
-            self.fail("subgenerator failed to raise ValueError")
-        self.assertEqual(trace,[
-            "Starting g",
-            "Yielded 0",
-            "Yielded 1",
-            "Yielded 2",
-            "Yielded 3",
-            "Yielded 4",
-            "Finishing g",
-        ])
-
-    def test_attempting_to_send_to_non_generator(self):
-        """
-        Test attempting to send to non-generator
-        """
-        trace = []
-        def g():
-            try:
-                trace.append("starting g")
-                yield from range(3)
-                trace.append("g should not be here")
-            finally:
-                trace.append("finishing g")
-        try:
-            gi = g()
-            next(gi)
-            for x in range(3):
-                y = gi.send(42)
-                trace.append("Should not have yielded: %s" % (y,))
-        except AttributeError as e:
-            self.assertIn("send", e.args[0])
-        else:
-            self.fail("was able to send into non-generator")
-        self.assertEqual(trace,[
-            "starting g",
-            "finishing g",
-        ])
-
-    def test_broken_getattr_handling(self):
-        """
-        Test subiterator with a broken getattr implementation
-        """
-        class Broken:
-            def __iter__(self):
-                return self
-            def __next__(self):
-                return 1
-            def __getattr__(self, attr):
-                1/0
-
-        def g():
-            yield from Broken()
-
-        with self.assertRaises(ZeroDivisionError):
-            gi = g()
-            self.assertEqual(next(gi), 1)
-            gi.send(1)
-
-        with self.assertRaises(ZeroDivisionError):
-            gi = g()
-            self.assertEqual(next(gi), 1)
-            gi.throw(AttributeError)
-
-# In MicroPython, exceptions is .close() are not ignored/printed to sys.stderr,
-# but propagated as usual.
-#        with captured_stderr() as output:
-#            gi = g()
-#            self.assertEqual(next(gi), 1)
-#            gi.close()
-#        self.assertIn('ZeroDivisionError', output.getvalue())
-
-    def test_exception_in_initial_next_call(self):
-        """
-        Test exception in initial next() call
-        """
-        trace = []
-        def g1():
-            trace.append("g1 about to yield from g2")
-            yield from g2()
-            trace.append("g1 should not be here")
-        def g2():
-            yield 1/0
-        def run():
-            gi = g1()
-            next(gi)
-        self.assertRaises(ZeroDivisionError,run)
-        self.assertEqual(trace,[
-            "g1 about to yield from g2"
-        ])
-
-    @unittest.skip("MicroPython doesn't check for already active generator when resuming it")
-    def test_attempted_yield_from_loop(self):
-        """
-        Test attempted yield-from loop
-        """
-        trace = []
-        def g1():
-            trace.append("g1: starting")
-            yield "y1"
-            trace.append("g1: about to yield from g2")
-            yield from g2()
-            trace.append("g1 should not be here")
-
-        def g2():
-            trace.append("g2: starting")
-            yield "y2"
-            trace.append("g2: about to yield from g1")
-            yield from gi
-            trace.append("g2 should not be here")
-        try:
-            gi = g1()
-            for y in gi:
-                trace.append("Yielded: %s" % (y,))
-        except ValueError as e:
-            self.assertEqual(e.args[0],"generator already executing")
-        else:
-            self.fail("subgenerator didn't raise ValueError")
-        self.assertEqual(trace,[
-            "g1: starting",
-            "Yielded: y1",
-            "g1: about to yield from g2",
-            "g2: starting",
-            "Yielded: y2",
-            "g2: about to yield from g1",
-        ])
-
-    def test_returning_value_from_delegated_throw(self):
-        """
-        Test returning value from delegated 'throw'
-        """
-        trace = []
-        def g1():
-            try:
-                trace.append("Starting g1")
-                yield "g1 ham"
-                yield from g2()
-                yield "g1 eggs"
-            finally:
-                trace.append("Finishing g1")
-        def g2():
-            try:
-                trace.append("Starting g2")
-                yield "g2 spam"
-                yield "g2 more spam"
-            except LunchError:
-                trace.append("Caught LunchError in g2")
-                yield "g2 lunch saved"
-                yield "g2 yet more spam"
-        class LunchError(Exception):
-            pass
-        g = g1()
-        for i in range(2):
-            x = next(g)
-            trace.append("Yielded %s" % (x,))
-        e = LunchError("tomato ejected")
-        g.throw(e)
-        for x in g:
-            trace.append("Yielded %s" % (x,))
-        self.assertEqual(trace,[
-            "Starting g1",
-            "Yielded g1 ham",
-            "Starting g2",
-            "Yielded g2 spam",
-            "Caught LunchError in g2",
-            "Yielded g2 yet more spam",
-            "Yielded g1 eggs",
-            "Finishing g1",
-        ])
-
-    def test_next_and_return_with_value(self):
-        """
-        Test next and return with value
-        """
-        trace = []
-        def f(r):
-            gi = g(r)
-            next(gi)
-            try:
-                trace.append("f resuming g")
-                next(gi)
-                trace.append("f SHOULD NOT BE HERE")
-            except StopIteration as e:
-                trace.append("f caught %s" % (repr(e),))
-        def g(r):
-            trace.append("g starting")
-            yield
-            trace.append("g returning %s" % (r,))
-            return r
-        f(None)
-        f(42)
-        self.assertEqual(trace,[
-            "g starting",
-            "f resuming g",
-            "g returning None",
-            "f caught StopIteration()",
-            "g starting",
-            "f resuming g",
-            "g returning 42",
-            "f caught StopIteration(42,)",
-        ])
-
-    def test_send_and_return_with_value(self):
-        """
-        Test send and return with value
-        """
-        trace = []
-        def f(r):
-            gi = g(r)
-            next(gi)
-            try:
-                trace.append("f sending spam to g")
-                gi.send("spam")
-                trace.append("f SHOULD NOT BE HERE")
-            except StopIteration as e:
-                trace.append("f caught %r" % (e,))
-        def g(r):
-            trace.append("g starting")
-            x = yield
-            trace.append("g received %s" % (x,))
-            trace.append("g returning %s" % (r,))
-            return r
-        f(None)
-        f(42)
-        self.assertEqual(trace,[
-            "g starting",
-            "f sending spam to g",
-            "g received spam",
-            "g returning None",
-            "f caught StopIteration()",
-            "g starting",
-            "f sending spam to g",
-            "g received spam",
-            "g returning 42",
-            "f caught StopIteration(42,)",
-        ])
-
-    def test_catching_exception_from_subgen_and_returning(self):
-        """
-        Test catching an exception thrown into a
-        subgenerator and returning a value
-        """
-        trace = []
-        def inner():
-            try:
-                yield 1
-            except ValueError:
-                trace.append("inner caught ValueError")
-            return 2
-
-        def outer():
-            v = yield from inner()
-            trace.append("inner returned %r to outer" % v)
-            yield v
-        g = outer()
-        trace.append(next(g))
-        trace.append(g.throw(ValueError))
-        self.assertEqual(trace,[
-            1,
-            "inner caught ValueError",
-            "inner returned 2 to outer",
-            2,
-        ])
-
-    def test_throwing_GeneratorExit_into_subgen_that_returns(self):
-        """
-        Test throwing GeneratorExit into a subgenerator that
-        catches it and returns normally.
-        """
-        trace = []
-        def f():
-            try:
-                trace.append("Enter f")
-                yield
-                trace.append("Exit f")
-            except GeneratorExit:
-                return
-        def g():
-            trace.append("Enter g")
-            yield from f()
-            trace.append("Exit g")
-        try:
-            gi = g()
-            next(gi)
-            gi.throw(GeneratorExit)
-        except GeneratorExit:
-            pass
-        else:
-            self.fail("subgenerator failed to raise GeneratorExit")
-        self.assertEqual(trace,[
-            "Enter g",
-            "Enter f",
-        ])
-
-    def test_throwing_GeneratorExit_into_subgenerator_that_yields(self):
-        """
-        Test throwing GeneratorExit into a subgenerator that
-        catches it and yields.
-        """
-        trace = []
-        def f():
-            try:
-                trace.append("Enter f")
-                yield
-                trace.append("Exit f")
-            except GeneratorExit:
-                yield
-        def g():
-            trace.append("Enter g")
-            yield from f()
-            trace.append("Exit g")
-        try:
-            gi = g()
-            next(gi)
-            gi.throw(GeneratorExit)
-        except RuntimeError as e:
-            self.assertEqual(e.args[0], "generator ignored GeneratorExit")
-        else:
-            self.fail("subgenerator failed to raise GeneratorExit")
-        self.assertEqual(trace,[
-            "Enter g",
-            "Enter f",
-        ])
-
-    def test_throwing_GeneratorExit_into_subgen_that_raises(self):
-        """
-        Test throwing GeneratorExit into a subgenerator that
-        catches it and raises a different exception.
-        """
-        trace = []
-        def f():
-            try:
-                trace.append("Enter f")
-                yield
-                trace.append("Exit f")
-            except GeneratorExit:
-                raise ValueError("Vorpal bunny encountered")
-        def g():
-            trace.append("Enter g")
-            yield from f()
-            trace.append("Exit g")
-        try:
-            gi = g()
-            next(gi)
-            gi.throw(GeneratorExit)
-        except ValueError as e:
-            self.assertEqual(e.args[0], "Vorpal bunny encountered")
-#            self.assertIsInstance(e.__context__, GeneratorExit)
-        else:
-            self.fail("subgenerator failed to raise ValueError")
-        self.assertEqual(trace,[
-            "Enter g",
-            "Enter f",
-        ])
-
-    def test_yield_from_empty(self):
-        def g():
-            yield from ()
-        self.assertRaises(StopIteration, next, g())
-
-    @unittest.skip("MicroPython doesn't check for already active generator when resuming it")
-    def test_delegating_generators_claim_to_be_running(self):
-        # Check with basic iteration
-        def one():
-            yield 0
-            yield from two()
-            yield 3
-        def two():
-            yield 1
-            try:
-                yield from g1
-            except ValueError:
-                pass
-            yield 2
-        g1 = one()
-        self.assertEqual(list(g1), [0, 1, 2, 3])
-        # Check with send
-        g1 = one()
-        res = [next(g1)]
-        try:
-            while True:
-                res.append(g1.send(42))
-        except StopIteration:
-            pass
-        self.assertEqual(res, [0, 1, 2, 3])
-        # Check with throw
-        class MyErr(Exception):
-            pass
-        def one():
-            try:
-                yield 0
-            except MyErr:
-                pass
-            yield from two()
-            try:
-                yield 3
-            except MyErr:
-                pass
-        def two():
-            try:
-                yield 1
-            except MyErr:
-                pass
-            try:
-                yield from g1
-            except ValueError:
-                pass
-            try:
-                yield 2
-            except MyErr:
-                pass
-        g1 = one()
-        res = [next(g1)]
-        try:
-            while True:
-                res.append(g1.throw(MyErr))
-        except StopIteration:
-            pass
-        # Check with close
-        class MyIt(object):
-            def __iter__(self):
-                return self
-            def __next__(self):
-                return 42
-            def close(self_):
-                self.assertTrue(g1.gi_running)
-                self.assertRaises(ValueError, next, g1)
-        def one():
-            yield from MyIt()
-        g1 = one()
-        next(g1)
-        g1.close()
-
-    @unittest.skip("MicroPython doesn't support inspect.stack()")
-    def test_delegator_is_visible_to_debugger(self):
-        def call_stack():
-            return [f[3] for f in inspect.stack()]
-
-        def gen():
-            yield call_stack()
-            yield call_stack()
-            yield call_stack()
-
-        def spam(g):
-            yield from g
-
-        def eggs(g):
-            yield from g
-
-        for stack in spam(gen()):
-            self.assertTrue('spam' in stack)
-
-        for stack in spam(eggs(gen())):
-            self.assertTrue('spam' in stack and 'eggs' in stack)
-
-    def test_custom_iterator_return(self):
-        # See issue #15568
-        class MyIter:
-            def __iter__(self):
-                return self
-            def __next__(self):
-                raise StopIteration(42)
-        def gen():
-            nonlocal ret
-            ret = yield from MyIter()
-        ret = None
-        list(gen())
-        self.assertEqual(ret, 42)
-
-    def test_close_with_cleared_frame(self):
-        # See issue #17669.
-        #
-        # Create a stack of generators: outer() delegating to inner()
-        # delegating to innermost(). The key point is that the instance of
-        # inner is created first: this ensures that its frame appears before
-        # the instance of outer in the GC linked list.
-        #
-        # At the gc.collect call:
-        #   - frame_clear is called on the inner_gen frame.
-        #   - gen_dealloc is called on the outer_gen generator (the only
-        #     reference is in the frame's locals).
-        #   - gen_close is called on the outer_gen generator.
-        #   - gen_close_iter is called to close the inner_gen generator, which
-        #     in turn calls gen_close, and gen_yf.
-        #
-        # Previously, gen_yf would crash since inner_gen's frame had been
-        # cleared (and in particular f_stacktop was NULL).
-
-        def innermost():
-            yield
-        def inner():
-            outer_gen = yield
-            yield from innermost()
-        def outer():
-            inner_gen = yield
-            yield from inner_gen
-
-        with disable_gc():
-            inner_gen = inner()
-            outer_gen = outer()
-            outer_gen.send(None)
-            outer_gen.send(inner_gen)
-            outer_gen.send(outer_gen)
-
-            del outer_gen
-            del inner_gen
-            gc_collect()
-
-    def test_send_tuple_with_custom_generator(self):
-        # See issue #21209.
-        class MyGen:
-            def __iter__(self):
-                return self
-            def __next__(self):
-                return 42
-            def send(self, what):
-                nonlocal v
-                v = what
-                return None
-        def outer():
-            v = yield from MyGen()
-        g = outer()
-        next(g)
-        v = None
-        g.send((1, 2, 3, 4))
-        self.assertEqual(v, (1, 2, 3, 4))
-
-
-def test_main():
-    from test import support
-    test_classes = [TestPEP380Operation]
-    support.run_unittest(*test_classes)
-
-
-if __name__ == '__main__':
-    test_main()
diff --git a/textwrap/metadata.txt b/textwrap/metadata.txt
deleted file mode 100644
index 065220a0d..000000000
--- a/textwrap/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = cpython
-type = module
-version = 3.4.2
diff --git a/textwrap/setup.py b/textwrap/setup.py
deleted file mode 100644
index 5dede890e..000000000
--- a/textwrap/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-textwrap',
-      version='3.4.2',
-      description='CPython textwrap module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['textwrap'])
diff --git a/threading/metadata.txt b/threading/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/threading/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/threading/setup.py b/threading/setup.py
deleted file mode 100644
index 5a2dc1b9c..000000000
--- a/threading/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-threading',
-      version='0.0.0',
-      description='Dummy threading module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['threading'])
diff --git a/threading/threading.py b/threading/threading.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/time/metadata.txt b/time/metadata.txt
deleted file mode 100644
index e07f78e8f..000000000
--- a/time/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.2.1
-depends = ffilib
diff --git a/time/setup.py b/time/setup.py
deleted file mode 100644
index c9b4dedf0..000000000
--- a/time/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-time',
-      version='0.2.1',
-      description='time module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['time'],
-      install_requires=['micropython-ffilib'])
diff --git a/time/time.py b/time/time.py
deleted file mode 100644
index 11b5ef4c9..000000000
--- a/time/time.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from utime import *
-import ffi
-import ffilib
-import array
-
-libc = ffilib.libc()
-
-# struct tm *gmtime(const time_t *timep);
-# struct tm *localtime(const time_t *timep);
-# size_t strftime(char *s, size_t max, const char *format,
-#                       const struct tm *tm);
-gmtime_ = libc.func("P", "gmtime", "P")
-localtime_ = libc.func("P", "localtime", "P")
-strftime_ = libc.func("i", "strftime", "sisP")
-
-
-def strftime(format, t=None):
-    if t is None:
-        t = time()
-
-    t = int(t)
-    a = array.array('i', [t])
-    tm_p = localtime_(a)
-    buf = bytearray(32)
-    l = strftime_(buf, 32, format, tm_p)
-    return str(buf[:l], "utf-8")
-
-def perf_counter():
-    return time()
-
-def process_time():
-    return clock()
diff --git a/timeit/metadata.txt b/timeit/metadata.txt
deleted file mode 100644
index 950962aef..000000000
--- a/timeit/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = cpython
-type = module
-version = 3.3.3-1
diff --git a/timeit/setup.py b/timeit/setup.py
deleted file mode 100644
index 9284aedf4..000000000
--- a/timeit/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-timeit',
-      version='3.3.3-1',
-      description='CPython timeit module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['timeit'])
diff --git a/tools/build.py b/tools/build.py
new file mode 100755
index 000000000..442cf2121
--- /dev/null
+++ b/tools/build.py
@@ -0,0 +1,462 @@
+#!/usr/bin/env python3
+#
+# This file is part of the MicroPython project, http://micropython.org/
+#
+# The MIT License (MIT)
+#
+# Copyright (c) 2022 Jim Mussared
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# This script compiles all packages in this repository (excluding unix-ffi)
+# into a directory suitable for serving to "mip" via a static web server.
+
+# Usage:
+# ./tools/build.py --output /tmp/micropython-lib/v2
+
+# The output directory (--output) will have the following layout
+# /
+#   index.json
+#   file/
+#     1d/
+#       1dddc25d
+#     c3/
+#       c31d7eb7
+#       c3a3934b
+#     e3/
+#       e39dbf64
+#     ...
+#   package/
+#     6/                 <-- mpy version
+#       aioble/
+#         latest.json
+#         0.1.json
+#         ...
+#       hmac/
+#         latest.json
+#         3.4.2-3.json
+#         ...
+#       pyjwt/
+#         latest.json
+#         0.1.json
+#         ...
+#     7/                 <-- other mpy versions
+#       ...
+#     py/                <-- "source" distribution
+#       ...
+#     ...
+
+# index.json is:
+# {
+#   "v": 2,                      <-- file format version
+#   "updated": <utc-seconds-since-1970>,
+#   "packages": {
+#     {
+#       "name": "aioble",
+#       "version": "0.1",        <-- Latest version of this package (always present, may be empty).
+#       "author": "",            <-- Optional author (always present, may be empty).
+#       "description": "...",    <-- Optional description (always present, may be empty).
+#       "license": "MIT",        <-- SPDX short identifier (required).
+#       "versions": {
+#         "6": ["0.1", "0.2"],
+#         "7": ["0.2", "0.3", "0.4"],
+#         ...                    <-- Other bytecode versions
+#         "py": ["0.1", "0.2", "0.3", "0.4"]
+#       },
+#       // The following entries were added in file format version 2.
+#       path: "micropython/bluetooth/aioble",
+#     },
+#     ...
+#   }
+# }
+
+# Each file in the "file" directory is the file contents (usually .mpy), named
+# by the prefix of the sha256 hash of the contents. Files are never removed, and
+# collisions are detected and will fail the compile, and the prefix length should
+# be increased.
+# As of September 2022, there are no collisions with a hash prefix length of 4,
+# so the default of 8 should be sufficient for a while. Increasing the length
+# doesn't invalidate old packages.
+
+# Each package json (either latest.json or {version}.json) is:
+# {
+#   "v": 1,     <-- file format version
+#   "hashes": [
+#     ["aioble/server.mpy", "e39dbf64"],
+#     ...
+#   ],
+#   "urls": [   <-- not used by micropython-lib packages
+#     ["target/path.py", "http://url/foo/bar/path.py"],
+#     ...
+#   ],
+#   "deps": [   <-- not used by micropython-lib packages
+#     ["name", "version"],
+#     ...
+#   ]
+#   "version": "0.1"
+# }
+
+# mip (or other tools) should request /package/{mpy_version}/{package_name}/{version}.json.
+
+import glob
+import hashlib
+import json
+import os
+import shutil
+import sys
+import tempfile
+import time
+
+
+_JSON_VERSION_INDEX = 2
+_JSON_VERSION_PACKAGE = 1
+
+
+_COLOR_ERROR_ON = "\033[1;31m"
+_COLOR_ERROR_OFF = "\033[0m"
+
+
+# Create all directories in the path (such that the file can be created).
+def ensure_path_exists(file_path):
+    path = os.path.dirname(file_path)
+    if not os.path.isdir(path):
+        os.makedirs(path)
+
+
+# Returns the sha256 of the specified file object.
+def _get_file_hash(f):
+    hs256 = hashlib.sha256()
+    hs256.update(f.read())
+    return hs256.hexdigest()
+
+
+# Returns true if the two files contain identical contents.
+def _identical_files(path_a, path_b):
+    with open(path_a, "rb") as fa:
+        with open(path_b, "rb") as fb:
+            return fa.read() == fb.read()
+
+
+# Helper to write the object as json to the specified path, creating any
+# directories as required.
+def _write_json(obj, path, minify=False):
+    ensure_path_exists(path)
+    with open(path, "w") as f:
+        json.dump(
+            obj, f, indent=(None if minify else 2), separators=((",", ":") if minify else None)
+        )
+        f.write("\n")
+
+
+# Write the package json to package/{"py" or mpy_version}/{package}/{version}.json.
+def _write_package_json(
+    package_json, out_package_dir, mpy_version, package_name, version, replace
+):
+    path = os.path.join(out_package_dir, mpy_version, package_name, version + ".json")
+    if replace or not os.path.exists(path):
+        _write_json(package_json, path, minify=True)
+
+
+# Format s with bold red.
+def error_color(s):
+    return _COLOR_ERROR_ON + s + _COLOR_ERROR_OFF
+
+
+# Copy src to "file"/{short_hash[0:2]}/{short_hash}.
+def _write_hashed_file(package_name, src, target_path, out_file_dir, hash_prefix_len):
+    # Generate the full sha256 and the hash prefix to use as the output path.
+    file_hash = _get_file_hash(src)
+    short_file_hash = file_hash[:hash_prefix_len]
+    # Group files into subdirectories using the first two bytes of the hash prefix.
+    output_file = os.path.join(short_file_hash[:2], short_file_hash)
+    output_file_path = os.path.join(out_file_dir, output_file)
+
+    if os.path.exists(output_file_path):
+        # If the file exists (e.g. from a previous run of this script), then ensure
+        # that it's actually the same file.
+        if not _identical_files(src.name, output_file_path):
+            print(
+                error_color("Hash collision processing:"),
+                package_name,
+                file=sys.stderr,
+            )
+            print("  File:        ", target_path, file=sys.stderr)
+            print("  Short hash:  ", short_file_hash, file=sys.stderr)
+            print("  Full hash:   ", file_hash, file=sys.stderr)
+            with open(output_file_path, "rb") as f:
+                print("  Target hash: ", _get_file_hash(f), file=sys.stderr)
+            print("Try increasing --hash-prefix (currently {})".format(hash_prefix_len))
+            sys.exit(1)
+    else:
+        # Create new file.
+        ensure_path_exists(output_file_path)
+        shutil.copyfile(src.name, output_file_path)
+
+    return short_file_hash
+
+
+# Convert the tagged .py file into a .mpy file and copy to the "file" output
+# directory with it's hashed name. Updates the package_json with the file
+# hash.
+def _compile_as_mpy(
+    package_name,
+    package_json,
+    tagged_path,
+    target_path,
+    opt,
+    mpy_cross,
+    mpy_cross_path,
+    out_file_dir,
+    hash_prefix_len,
+):
+    with tempfile.NamedTemporaryFile(mode="rb", suffix=".mpy", delete=True) as mpy_tempfile:
+        try:
+            mpy_cross.compile(
+                tagged_path,
+                dest=mpy_tempfile.name,
+                src_path=target_path,
+                opt=opt,
+                mpy_cross=mpy_cross_path,
+            )
+        except mpy_cross.CrossCompileError as e:
+            print(
+                error_color("Error:"),
+                "Unable to compile",
+                target_path,
+                "in package",
+                package_name,
+                file=sys.stderr,
+            )
+            print(e)
+            sys.exit(1)
+
+        short_mpy_hash = _write_hashed_file(
+            package_name, mpy_tempfile, target_path, out_file_dir, hash_prefix_len
+        )
+
+        # Add the file to the package json.
+        target_path_mpy = target_path[:-2] + "mpy"
+        package_json["hashes"].append((target_path_mpy, short_mpy_hash))
+
+
+# Copy the tagged .py file to the "file" output directory with it's hashed
+# name. Updates the package_json with the file hash.
+def _copy_as_py(
+    package_name, package_json, tagged_path, target_path, out_file_dir, hash_prefix_len
+):
+    with open(tagged_path, "rb") as tagged_file:
+        short_py_hash = _write_hashed_file(
+            package_name, tagged_file, target_path, out_file_dir, hash_prefix_len
+        )
+    # Add the file to the package json.
+    package_json["hashes"].append((target_path, short_py_hash))
+
+
+# Update to the latest metadata, and add any new versions to the package in
+# the index json.
+def _update_index_package_metadata(index_package_json, metadata, mpy_version, package_path):
+    index_package_json["version"] = metadata.version or ""
+    index_package_json["author"] = ""  # TODO: Make manifestfile.py capture this.
+    index_package_json["description"] = metadata.description or ""
+    index_package_json["license"] = metadata.license or "MIT"
+    if "versions" not in index_package_json:
+        index_package_json["versions"] = {}
+    if metadata.version:
+        for v in ("py", mpy_version):
+            if v not in index_package_json["versions"]:
+                index_package_json["versions"][v] = []
+            if metadata.version not in index_package_json["versions"][v]:
+                print("  New version {}={}".format(v, metadata.version))
+                index_package_json["versions"][v].append(metadata.version)
+
+    # The following entries were added in file format version 2.
+    index_package_json["path"] = package_path
+
+
+def build(output_path, hash_prefix_len, mpy_cross_path):
+    import manifestfile
+    import mpy_cross
+
+    out_file_dir = os.path.join(output_path, "file")
+    out_package_dir = os.path.join(output_path, "package")
+
+    path_vars = {
+        "MPY_LIB_DIR": os.path.abspath(os.path.join(os.path.dirname(__file__), "..")),
+    }
+
+    index_json_path = os.path.join(output_path, "index.json")
+
+    try:
+        with open(index_json_path) as f:
+            print("Updating existing index.json")
+            index_json = json.load(f)
+    except FileNotFoundError:
+        print("Creating new index.json")
+        index_json = {"packages": []}
+
+    index_json["v"] = _JSON_VERSION_INDEX
+    index_json["updated"] = int(time.time())
+
+    # For now, don't process unix-ffi. In the future this can be extended to
+    # allow a way to request unix-ffi packages via mip.
+    lib_dirs = ["micropython", "python-stdlib", "python-ecosys"]
+
+    mpy_version, _mpy_sub_version = mpy_cross.mpy_version(mpy_cross=mpy_cross_path)
+    mpy_version = str(mpy_version)
+    print("Generating bytecode version", mpy_version)
+
+    for lib_dir in lib_dirs:
+        for manifest_path in glob.glob(os.path.join(lib_dir, "**", "manifest.py"), recursive=True):
+            package_path = os.path.dirname(manifest_path)
+            print("{}".format(package_path))
+            # .../foo/manifest.py -> foo
+            package_name = os.path.basename(os.path.dirname(manifest_path))
+
+            # Compile the manifest.
+            manifest = manifestfile.ManifestFile(manifestfile.MODE_COMPILE, path_vars)
+            manifest.execute(manifest_path)
+
+            # Append this package to the index.
+            if not manifest.metadata().version:
+                print(error_color("Warning:"), package_name, "doesn't have a version.")
+
+            # Try to find this package in the previous index.json.
+            for p in index_json["packages"]:
+                if p["name"] == package_name:
+                    index_package_json = p
+                    break
+            else:
+                print("  First-time package")
+                index_package_json = {
+                    "name": package_name,
+                }
+                index_json["packages"].append(index_package_json)
+
+            _update_index_package_metadata(
+                index_package_json, manifest.metadata(), mpy_version, package_path
+            )
+
+            # This is the package json that mip/mpremote downloads.
+            mpy_package_json = {
+                "v": _JSON_VERSION_PACKAGE,
+                "hashes": [],
+                "version": manifest.metadata().version or "",
+            }
+            py_package_json = {
+                "v": _JSON_VERSION_PACKAGE,
+                "hashes": [],
+                "version": manifest.metadata().version or "",
+            }
+
+            for result in manifest.files():
+                # This isn't allowed in micropython-lib anyway.
+                if result.file_type != manifestfile.FILE_TYPE_LOCAL:
+                    print(error_color("Error:"), "Non-local file not supported.", file=sys.stderr)
+                    sys.exit(1)
+
+                if not result.target_path.endswith(".py"):
+                    print(
+                        error_color("Error:"),
+                        "Target path isn't a .py file:",
+                        result.target_path,
+                        file=sys.stderr,
+                    )
+                    sys.exit(1)
+
+                # Tag each file with the package metadata and compile to .mpy
+                # (and copy the .py directly).
+                with manifestfile.tagged_py_file(result.full_path, result.metadata) as tagged_path:
+                    _compile_as_mpy(
+                        package_name,
+                        mpy_package_json,
+                        tagged_path,
+                        result.target_path,
+                        result.opt,
+                        mpy_cross,
+                        mpy_cross_path,
+                        out_file_dir,
+                        hash_prefix_len,
+                    )
+                    _copy_as_py(
+                        package_name,
+                        py_package_json,
+                        tagged_path,
+                        result.target_path,
+                        out_file_dir,
+                        hash_prefix_len,
+                    )
+
+            # Create/replace {package}/latest.json.
+            _write_package_json(
+                mpy_package_json,
+                out_package_dir,
+                mpy_version,
+                package_name,
+                "latest",
+                replace=True,
+            )
+            _write_package_json(
+                py_package_json, out_package_dir, "py", package_name, "latest", replace=True
+            )
+
+            # Write {package}/{version}.json, but only if it doesn't already
+            # exist. A package version is "locked" the first time it's seen
+            # by this script.
+            if manifest.metadata().version:
+                _write_package_json(
+                    mpy_package_json,
+                    out_package_dir,
+                    mpy_version,
+                    package_name,
+                    manifest.metadata().version,
+                    replace=False,
+                )
+                _write_package_json(
+                    py_package_json,
+                    out_package_dir,
+                    "py",
+                    package_name,
+                    manifest.metadata().version,
+                    replace=False,
+                )
+
+    # Write updated package index json, sorted by package name.
+    index_json["packages"].sort(key=lambda p: p["name"])
+    _write_json(index_json, index_json_path, minify=False)
+
+
+def main():
+    import argparse
+
+    cmd_parser = argparse.ArgumentParser(description="Compile micropython-lib for serving to mip.")
+    cmd_parser.add_argument("--output", required=True, help="output directory")
+    cmd_parser.add_argument("--hash-prefix", default=8, type=int, help="hash prefix length")
+    cmd_parser.add_argument("--mpy-cross", default=None, help="optional path to mpy-cross binary")
+    cmd_parser.add_argument("--micropython", default=None, help="path to micropython repo")
+    args = cmd_parser.parse_args()
+
+    if args.micropython:
+        sys.path.append(os.path.join(args.micropython, "tools"))  # for manifestfile
+        sys.path.append(os.path.join(args.micropython, "mpy-cross"))  # for mpy_cross
+
+    build(args.output, hash_prefix_len=max(4, args.hash_prefix), mpy_cross_path=args.mpy_cross)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/ci.sh b/tools/ci.sh
new file mode 100755
index 000000000..6689e8aa4
--- /dev/null
+++ b/tools/ci.sh
@@ -0,0 +1,242 @@
+#!/bin/bash
+
+CP=/bin/cp
+
+########################################################################################
+# commit formatting
+
+function ci_commit_formatting_run {
+    git remote add upstream https://github.com/micropython/micropython-lib.git
+    git fetch --depth=100 upstream master
+    # If the common ancestor commit hasn't been found, fetch more.
+    git merge-base upstream/master HEAD || git fetch --unshallow upstream master
+    # For a PR, upstream/master..HEAD ends with a merge commit into master, exclude that one.
+    tools/verifygitlog.py -v upstream/master..HEAD --no-merges
+}
+
+########################################################################################
+# package tests
+
+MICROPYTHON=/tmp/micropython/ports/unix/build-standard/micropython
+
+function ci_package_tests_setup_micropython {
+    git clone https://github.com/micropython/micropython.git /tmp/micropython
+
+    # build mpy-cross and micropython (use -O0 to speed up the build)
+    make -C /tmp/micropython/mpy-cross -j CFLAGS_EXTRA=-O0
+    make -C /tmp/micropython/ports/unix submodules
+    make -C /tmp/micropython/ports/unix -j CFLAGS_EXTRA=-O0
+}
+
+function ci_package_tests_setup_lib {
+    mkdir -p ~/.micropython/lib
+    $CP micropython/ucontextlib/ucontextlib.py ~/.micropython/lib/
+    $CP python-stdlib/fnmatch/fnmatch.py ~/.micropython/lib/
+    $CP -r python-stdlib/hashlib-core/hashlib ~/.micropython/lib/
+    $CP -r python-stdlib/hashlib-sha224/hashlib ~/.micropython/lib/
+    $CP -r python-stdlib/hashlib-sha256/hashlib ~/.micropython/lib/
+    $CP -r python-stdlib/hashlib-sha384/hashlib ~/.micropython/lib/
+    $CP -r python-stdlib/hashlib-sha512/hashlib ~/.micropython/lib/
+    $CP python-stdlib/shutil/shutil.py ~/.micropython/lib/
+    $CP python-stdlib/tempfile/tempfile.py ~/.micropython/lib/
+    $CP -r python-stdlib/unittest/unittest ~/.micropython/lib/
+    $CP -r python-stdlib/unittest-discover/unittest ~/.micropython/lib/
+    $CP unix-ffi/ffilib/ffilib.py ~/.micropython/lib/
+    tree ~/.micropython
+}
+
+function ci_package_tests_run {
+    for test in \
+        micropython/drivers/storage/sdcard/sdtest.py \
+        micropython/xmltok/test_xmltok.py \
+        python-ecosys/requests/test_requests.py \
+        python-stdlib/argparse/test_argparse.py \
+        python-stdlib/base64/test_base64.py \
+        python-stdlib/binascii/test_binascii.py \
+        python-stdlib/collections-defaultdict/test_defaultdict.py \
+        python-stdlib/functools/test_partial.py \
+        python-stdlib/functools/test_reduce.py \
+        python-stdlib/heapq/test_heapq.py \
+        python-stdlib/hmac/test_hmac.py \
+        python-stdlib/itertools/test_itertools.py \
+        python-stdlib/operator/test_operator.py \
+        python-stdlib/os-path/test_path.py \
+        python-stdlib/pickle/test_pickle.py \
+        python-stdlib/string/test_translate.py \
+        python-stdlib/unittest/tests/exception.py \
+        unix-ffi/gettext/test_gettext.py \
+        unix-ffi/pwd/test_getpwnam.py \
+        unix-ffi/re/test_re.py \
+        unix-ffi/sqlite3/test_sqlite3.py \
+        unix-ffi/sqlite3/test_sqlite3_2.py \
+        unix-ffi/sqlite3/test_sqlite3_3.py \
+        unix-ffi/time/test_strftime.py \
+        ; do
+        echo "Running test $test"
+        (cd `dirname $test` && $MICROPYTHON `basename $test`)
+        if [ $? -ne 0 ]; then
+            false # make this function return an error code
+            return
+        fi
+    done
+
+    for path in \
+        micropython/ucontextlib \
+        python-stdlib/contextlib \
+        python-stdlib/datetime \
+        python-stdlib/fnmatch \
+        python-stdlib/hashlib \
+        python-stdlib/inspect \
+        python-stdlib/pathlib \
+        python-stdlib/quopri \
+        python-stdlib/shutil \
+        python-stdlib/tempfile \
+        python-stdlib/time \
+        python-stdlib/unittest/tests \
+        python-stdlib/unittest-discover/tests \
+        ; do
+        (cd $path && $MICROPYTHON -m unittest)
+        if [ $? -ne 0 ]; then false; return; fi
+    done
+
+    (cd micropython/usb/usb-device && $MICROPYTHON -m tests.test_core_buffer)
+    if [ $? -ne 0 ]; then false; return; fi
+
+    (cd python-ecosys/cbor2 && $MICROPYTHON -m examples.cbor_test)
+    if [ $? -ne 0 ]; then false; return; fi
+}
+
+########################################################################################
+# build packages
+
+function ci_build_packages_setup {
+    git clone https://github.com/micropython/micropython.git /tmp/micropython
+
+    # build mpy-cross (use -O0 to speed up the build)
+    make -C /tmp/micropython/mpy-cross -j CFLAGS_EXTRA=-O0
+
+    # check the required programs run
+    /tmp/micropython/mpy-cross/build/mpy-cross --version
+    python3 /tmp/micropython/tools/manifestfile.py --help
+}
+
+function ci_build_packages_check_manifest {
+    for file in $(find -name manifest.py); do
+        echo "##################################################"
+        echo "# Testing $file"
+        extra_args=
+        if [[ "$file" =~ "/unix-ffi/" ]]; then
+            extra_args="--unix-ffi"
+        fi
+        python3 /tmp/micropython/tools/manifestfile.py $extra_args --lib . --compile $file
+    done
+}
+
+function ci_build_packages_compile_index {
+    python3 tools/build.py --micropython /tmp/micropython --output $PACKAGE_INDEX_PATH
+}
+
+function ci_build_packages_examples {
+    for example in $(find -path \*example\*.py); do
+        /tmp/micropython/mpy-cross/build/mpy-cross $example
+    done
+}
+
+function ci_push_package_index {
+    set -euo pipefail
+
+    # Note: This feature is opt-in, so this function is only run by GitHub
+    # Actions if the MICROPY_PUBLISH_MIP_INDEX repository variable is set to a
+    # "truthy" value in the "Secrets and variables" -> "Actions"
+    # -> "Variables" setting of the GitHub repo.
+
+    PAGES_PATH=/tmp/gh-pages
+
+    if git fetch --depth=1 origin gh-pages; then
+        git worktree add ${PAGES_PATH} gh-pages
+        cd ${PAGES_PATH}
+        NEW_BRANCH=0
+    else
+        echo "Creating gh-pages branch for $GITHUB_REPOSITORY..."
+        git worktree add --force ${PAGES_PATH} HEAD
+        cd ${PAGES_PATH}
+        git switch --orphan gh-pages
+        NEW_BRANCH=1
+    fi
+
+    DEST_PATH=${PAGES_PATH}/mip/${GITHUB_REF_NAME}
+    if [ -d ${DEST_PATH} ]; then
+        git rm -r ${DEST_PATH}
+    fi
+    mkdir -p ${DEST_PATH}
+    cd ${DEST_PATH}
+
+    cp -r ${PACKAGE_INDEX_PATH}/* .
+
+    git add .
+    git_bot_commit "Add CI built packages from commit ${GITHUB_SHA} of ${GITHUB_REF_NAME}"
+
+    if [ "$NEW_BRANCH" -eq 0 ]; then
+        # A small race condition exists here if another CI job pushes to
+        # gh-pages at the same time, but this narrows the race to the time
+        # between these two commands.
+        git pull --rebase origin gh-pages
+    fi
+    git push origin gh-pages
+
+    INDEX_URL="https://${GITHUB_REPOSITORY_OWNER}.github.io/$(echo ${GITHUB_REPOSITORY} | cut -d'/' -f2-)/mip/${GITHUB_REF_NAME}"
+
+    echo ""
+    echo "--------------------------------------------------"
+    echo "Uploaded package files to GitHub Pages."
+    echo ""
+    echo "Unless GitHub Pages is disabled on this repo, these files can be installed remotely with:"
+    echo ""
+    echo "mpremote mip install --index ${INDEX_URL} PACKAGE_NAME"
+    echo ""
+    echo "or on the device as:"
+    echo ""
+    echo "import mip"
+    echo "mip.install(PACKAGE_NAME, index=\"${INDEX_URL}\")"
+}
+
+function ci_cleanup_package_index()
+{
+    if ! git fetch --depth=1 origin gh-pages; then
+        exit 0
+    fi
+
+    # Argument $1 is github.event.ref, passed in from workflow file.
+    #
+    # this value seems to be a REF_NAME, without heads/ or tags/ prefix. (Can't
+    # use GITHUB_REF_NAME, this evaluates to the default branch.)
+    DELETED_REF="$1"
+
+    if [ -z "$DELETED_REF" ]; then
+        echo "Bad DELETE_REF $DELETED_REF"
+        exit 1  # Internal error with ref format, better than removing all mip/ directory in a commit
+    fi
+
+    # We need Actions to check out default branch and run tools/ci.sh, but then
+    # we switch branches
+    git switch gh-pages
+
+    echo "Removing any published packages for ${DELETED_REF}..."
+    if [ -d mip/${DELETED_REF} ]; then
+        git rm -r mip/${DELETED_REF}
+        git_bot_commit "Remove CI built packages from deleted ${DELETED_REF}"
+        git pull --rebase origin gh-pages
+        git push origin gh-pages
+    else
+        echo "Nothing to remove."
+    fi
+}
+
+# Make a git commit with bot authorship
+# Argument $1 is the commit message
+function git_bot_commit {
+    # Ref https://github.com/actions/checkout/discussions/479
+    git config user.name 'github-actions[bot]'
+    git config user.email 'github-actions[bot]@users.noreply.github.com'
+    git commit -m "$1"
+}
diff --git a/tools/codeformat.py b/tools/codeformat.py
new file mode 100755
index 000000000..6a7f2b35f
--- /dev/null
+++ b/tools/codeformat.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+#
+# This file is part of the MicroPython project, http://micropython.org/
+#
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Damien P. George
+# Copyright (c) 2023 Jim Mussared
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# This is just a wrapper around running ruff format, so that code formatting can be
+# invoked in the same way as in the main repo.
+
+import os
+import subprocess
+
+# Path to repo top-level dir.
+TOP = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+
+
+def main():
+    command = ["ruff", "format", "."]
+    subprocess.check_call(command, cwd=TOP)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/makepyproject.py b/tools/makepyproject.py
new file mode 100755
index 000000000..25c05d05f
--- /dev/null
+++ b/tools/makepyproject.py
@@ -0,0 +1,213 @@
+#!/usr/bin/env python3
+#
+# This file is part of the MicroPython project, http://micropython.org/
+#
+# The MIT License (MIT)
+#
+# Copyright (c) 2023 Jim Mussared
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# This script makes a CPython-compatible package from a micropython-lib package
+# with a pyproject.toml that can be built (via hatch) and deployed to PyPI.
+# Requires that the project sets the pypi_publish= kwarg in its metadata().
+
+# Usage:
+# ./tools/makepyproject.py --output /tmp/foo micropython/foo
+# python -m build /tmp/foo
+# python -m twine upload /tmp/foo/dist/*.whl
+
+from email.utils import parseaddr
+import os
+import re
+import shutil
+import sys
+
+from build import error_color, ensure_path_exists
+
+
+DEFAULT_AUTHOR = "micropython-lib <contact@micropython.org>"
+DEFAULT_LICENSE = "MIT"
+
+
+def quoted_escape(s):
+    return s.replace('"', '\\"')
+
+
+def build(manifest_path, output_path):
+    import manifestfile
+
+    if not manifest_path.endswith(".py"):
+        # Allow specifying either the directory or the manifest file explicitly.
+        manifest_path = os.path.join(manifest_path, "manifest.py")
+
+    print("Generating pyproject for {} in {}...".format(manifest_path, output_path))
+
+    toml_path = os.path.join(output_path, "pyproject.toml")
+    ensure_path_exists(toml_path)
+
+    path_vars = {
+        "MPY_LIB_DIR": os.path.abspath(os.path.join(os.path.dirname(__file__), "..")),
+    }
+
+    # .../foo/manifest.py -> foo
+    package_name = os.path.basename(os.path.dirname(manifest_path))
+
+    # Compile the manifest.
+    manifest = manifestfile.ManifestFile(manifestfile.MODE_PYPROJECT, path_vars)
+    manifest.execute(manifest_path)
+
+    # If a package doesn't have a pypi name, then assume it isn't intended to
+    # be publishable.
+    if not manifest.metadata().pypi_publish:
+        print(error_color("Error:"), package_name, "doesn't have a pypi_publish name.")
+        sys.exit(1)
+
+    # These should be in all packages eventually.
+    if not manifest.metadata().version:
+        print(error_color("Error:"), package_name, "doesn't have a version.")
+        sys.exit(1)
+    if not manifest.metadata().description:
+        print(error_color("Error:"), package_name, "doesn't have a description.")
+        sys.exit(1)
+
+    # This is the root path of all .py files that are copied. We ensure that
+    # they all match.
+    top_level_package = None
+
+    for result in manifest.files():
+        # This isn't allowed in micropython-lib anyway.
+        if result.file_type != manifestfile.FILE_TYPE_LOCAL:
+            print(error_color("Error:"), "Non-local file not supported.", file=sys.stderr)
+            sys.exit(1)
+
+        # "foo/bar/baz.py" --> "foo"
+        # "baz.py" --> ""
+        result_package = os.path.split(result.target_path)[0]
+
+        if not result_package:
+            # This is a standalone .py file.
+            print(
+                error_color("Error:"),
+                "Unsupported single-file module: {}".format(result.target_path),
+                file=sys.stderr,
+            )
+            sys.exit(1)
+        if top_level_package and result_package != top_level_package:
+            # This likely suggests that something needs to use require(..., pypi="...").
+            print(
+                error_color("Error:"),
+                "More than one top-level package: {}, {}.".format(
+                    result_package, top_level_package
+                ),
+                file=sys.stderr,
+            )
+            sys.exit(1)
+        top_level_package = result_package
+
+        # Tag each file with the package metadata and copy the .py directly.
+        with manifestfile.tagged_py_file(result.full_path, result.metadata) as tagged_path:
+            dest_path = os.path.join(output_path, result.target_path)
+            ensure_path_exists(dest_path)
+            shutil.copyfile(tagged_path, dest_path)
+
+    # Copy README.md if it exists
+    readme_path = os.path.join(os.path.dirname(manifest_path), "README.md")
+    readme_toml = ""
+    if os.path.exists(readme_path):
+        shutil.copyfile(readme_path, os.path.join(output_path, "README.md"))
+        readme_toml = 'readme = "README.md"'
+
+    # Apply default author and license, otherwise use the package metadata.
+    license_toml = 'license = {{ text = "{}" }}'.format(
+        quoted_escape(manifest.metadata().license or DEFAULT_LICENSE)
+    )
+    author_name, author_email = parseaddr(manifest.metadata().author or DEFAULT_AUTHOR)
+    author_toml = 'authors = [ {{ name = "{}", email = "{}"}} ]'.format(
+        quoted_escape(author_name), quoted_escape(author_email)
+    )
+
+    # Write pyproject.toml.
+    with open(toml_path, "w") as toml_file:
+        print("# Generated by makepyproject.py", file=toml_file)
+
+        print(
+            """
+[build-system]
+requires = [
+    "hatchling"
+]
+build-backend = "hatchling.build"
+""",
+            file=toml_file,
+        )
+
+        print(
+            """
+[project]
+name = "{}"
+description = "{}"
+{}
+{}
+version = "{}"
+dependencies = [{}]
+urls = {{ Homepage = "https://github.com/micropython/micropython-lib" }}
+{}
+""".format(
+                quoted_escape(manifest.metadata().pypi_publish),
+                quoted_escape(manifest.metadata().description),
+                author_toml,
+                license_toml,
+                quoted_escape(manifest.metadata().version),
+                ", ".join('"{}"'.format(quoted_escape(r)) for r in manifest.pypi_dependencies()),
+                readme_toml,
+            ),
+            file=toml_file,
+        )
+
+        print(
+            """
+[tool.hatch.build]
+packages = ["{}"]
+""".format(top_level_package),
+            file=toml_file,
+        )
+
+    print("Done.")
+
+
+def main():
+    import argparse
+
+    cmd_parser = argparse.ArgumentParser(
+        description="Generate a project that can be pushed to PyPI."
+    )
+    cmd_parser.add_argument("--output", required=True, help="output directory")
+    cmd_parser.add_argument("--micropython", default=None, help="path to micropython repo")
+    cmd_parser.add_argument("manifest", help="input package path")
+    args = cmd_parser.parse_args()
+
+    if args.micropython:
+        sys.path.append(os.path.join(args.micropython, "tools"))  # for manifestfile
+
+    build(args.manifest, args.output)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/uncrustify.cfg b/tools/uncrustify.cfg
new file mode 100644
index 000000000..80542b903
--- /dev/null
+++ b/tools/uncrustify.cfg
@@ -0,0 +1,3093 @@
+# Uncrustify-0.71.0_f
+
+#
+# General options
+#
+
+# The type of line endings.
+#
+# Default: auto
+newlines                        = auto     # lf/crlf/cr/auto
+
+# The original size of tabs in the input.
+#
+# Default: 8
+input_tab_size                  = 8        # unsigned number
+
+# The size of tabs in the output (only used if align_with_tabs=true).
+#
+# Default: 8
+output_tab_size                 = 8        # unsigned number
+
+# The ASCII value of the string escape char, usually 92 (\) or (Pawn) 94 (^).
+#
+# Default: 92
+string_escape_char              = 92       # unsigned number
+
+# Alternate string escape char (usually only used for Pawn).
+# Only works right before the quote char.
+string_escape_char2             = 0        # unsigned number
+
+# Replace tab characters found in string literals with the escape sequence \t
+# instead.
+string_replace_tab_chars        = false    # true/false
+
+# Allow interpreting '>=' and '>>=' as part of a template in code like
+# 'void f(list<list<B>>=val);'. If true, 'assert(x<0 && y>=3)' will be broken.
+# Improvements to template detection may make this option obsolete.
+tok_split_gte                   = false    # true/false
+
+# Disable formatting of NL_CONT ('\\n') ended lines (e.g. multiline macros)
+disable_processing_nl_cont      = false    # true/false
+
+# Specify the marker used in comments to disable processing of part of the
+# file.
+# The comment should be used alone in one line.
+#
+# Default:  *INDENT-OFF*
+disable_processing_cmt          = " *FORMAT-OFF*"      # string
+
+# Specify the marker used in comments to (re)enable processing in a file.
+# The comment should be used alone in one line.
+#
+# Default:  *INDENT-ON*
+enable_processing_cmt           = " *FORMAT-ON*"     # string
+
+# Enable parsing of digraphs.
+enable_digraphs                 = false    # true/false
+
+# Add or remove the UTF-8 BOM (recommend 'remove').
+utf8_bom                        = ignore   # ignore/add/remove/force
+
+# If the file contains bytes with values between 128 and 255, but is not
+# UTF-8, then output as UTF-8.
+utf8_byte                       = false    # true/false
+
+# Force the output encoding to UTF-8.
+utf8_force                      = false    # true/false
+
+# Add or remove space between 'do' and '{'.
+sp_do_brace_open                = force    # ignore/add/remove/force
+
+# Add or remove space between '}' and 'while'.
+sp_brace_close_while            = force    # ignore/add/remove/force
+
+# Add or remove space between 'while' and '('.
+sp_while_paren_open             = force    # ignore/add/remove/force
+
+#
+# Spacing options
+#
+
+# Add or remove space around non-assignment symbolic operators ('+', '/', '%',
+# '<<', and so forth).
+sp_arith                        = force    # ignore/add/remove/force
+
+# Add or remove space around arithmetic operators '+' and '-'.
+#
+# Overrides sp_arith.
+sp_arith_additive               = force    # ignore/add/remove/force
+
+# Add or remove space around assignment operator '=', '+=', etc.
+sp_assign                       = force    # ignore/add/remove/force
+
+# Add or remove space around '=' in C++11 lambda capture specifications.
+#
+# Overrides sp_assign.
+sp_cpp_lambda_assign            = ignore   # ignore/add/remove/force
+
+# Add or remove space after the capture specification of a C++11 lambda when
+# an argument list is present, as in '[] <here> (int x){ ... }'.
+sp_cpp_lambda_square_paren      = ignore   # ignore/add/remove/force
+
+# Add or remove space after the capture specification of a C++11 lambda with
+# no argument list is present, as in '[] <here> { ... }'.
+sp_cpp_lambda_square_brace      = ignore   # ignore/add/remove/force
+
+# Add or remove space after the argument list of a C++11 lambda, as in
+# '[](int x) <here> { ... }'.
+sp_cpp_lambda_paren_brace       = ignore   # ignore/add/remove/force
+
+# Add or remove space between a lambda body and its call operator of an
+# immediately invoked lambda, as in '[]( ... ){ ... } <here> ( ... )'.
+sp_cpp_lambda_fparen            = ignore   # ignore/add/remove/force
+
+# Add or remove space around assignment operator '=' in a prototype.
+#
+# If set to ignore, use sp_assign.
+sp_assign_default               = ignore   # ignore/add/remove/force
+
+# Add or remove space before assignment operator '=', '+=', etc.
+#
+# Overrides sp_assign.
+sp_before_assign                = ignore   # ignore/add/remove/force
+
+# Add or remove space after assignment operator '=', '+=', etc.
+#
+# Overrides sp_assign.
+sp_after_assign                 = ignore   # ignore/add/remove/force
+
+# Add or remove space in 'NS_ENUM ('.
+sp_enum_paren                   = ignore   # ignore/add/remove/force
+
+# Add or remove space around assignment '=' in enum.
+sp_enum_assign                  = ignore   # ignore/add/remove/force
+
+# Add or remove space before assignment '=' in enum.
+#
+# Overrides sp_enum_assign.
+sp_enum_before_assign           = ignore   # ignore/add/remove/force
+
+# Add or remove space after assignment '=' in enum.
+#
+# Overrides sp_enum_assign.
+sp_enum_after_assign            = ignore   # ignore/add/remove/force
+
+# Add or remove space around assignment ':' in enum.
+sp_enum_colon                   = ignore   # ignore/add/remove/force
+
+# Add or remove space around preprocessor '##' concatenation operator.
+#
+# Default: add
+sp_pp_concat                    = remove   # ignore/add/remove/force
+
+# Add or remove space after preprocessor '#' stringify operator.
+# Also affects the '#@' charizing operator.
+sp_pp_stringify                 = ignore   # ignore/add/remove/force
+
+# Add or remove space before preprocessor '#' stringify operator
+# as in '#define x(y) L#y'.
+sp_before_pp_stringify          = ignore   # ignore/add/remove/force
+
+# Add or remove space around boolean operators '&&' and '||'.
+sp_bool                         = force    # ignore/add/remove/force
+
+# Add or remove space around compare operator '<', '>', '==', etc.
+sp_compare                      = force    # ignore/add/remove/force
+
+# Add or remove space inside '(' and ')'.
+sp_inside_paren                 = remove   # ignore/add/remove/force
+
+# Add or remove space between nested parentheses, i.e. '((' vs. ') )'.
+sp_paren_paren                  = remove   # ignore/add/remove/force
+
+# Add or remove space between back-to-back parentheses, i.e. ')(' vs. ') ('.
+sp_cparen_oparen                = ignore   # ignore/add/remove/force
+
+# Whether to balance spaces inside nested parentheses.
+sp_balance_nested_parens        = false    # true/false
+
+# Add or remove space between ')' and '{'.
+sp_paren_brace                  = force    # ignore/add/remove/force
+
+# Add or remove space between nested braces, i.e. '{{' vs '{ {'.
+sp_brace_brace                  = force    # ignore/add/remove/force
+
+# Add or remove space before pointer star '*'.
+sp_before_ptr_star              = force    # ignore/add/remove/force
+
+# Add or remove space before pointer star '*' that isn't followed by a
+# variable name. If set to ignore, sp_before_ptr_star is used instead.
+sp_before_unnamed_ptr_star      = force    # ignore/add/remove/force
+
+# Add or remove space between pointer stars '*'.
+sp_between_ptr_star             = remove   # ignore/add/remove/force
+
+# Add or remove space after pointer star '*', if followed by a word.
+#
+# Overrides sp_type_func.
+sp_after_ptr_star               = remove   # ignore/add/remove/force
+
+# Add or remove space after pointer caret '^', if followed by a word.
+sp_after_ptr_block_caret        = ignore   # ignore/add/remove/force
+
+# Add or remove space after pointer star '*', if followed by a qualifier.
+sp_after_ptr_star_qualifier     = remove   # ignore/add/remove/force
+
+# Add or remove space after a pointer star '*', if followed by a function
+# prototype or function definition.
+#
+# Overrides sp_after_ptr_star and sp_type_func.
+sp_after_ptr_star_func          = ignore   # ignore/add/remove/force
+
+# Add or remove space after a pointer star '*', if followed by an open
+# parenthesis, as in 'void* (*)().
+sp_ptr_star_paren               = ignore   # ignore/add/remove/force
+
+# Add or remove space before a pointer star '*', if followed by a function
+# prototype or function definition.
+sp_before_ptr_star_func         = force    # ignore/add/remove/force
+
+# Add or remove space before a reference sign '&'.
+sp_before_byref                 = ignore   # ignore/add/remove/force
+
+# Add or remove space before a reference sign '&' that isn't followed by a
+# variable name. If set to ignore, sp_before_byref is used instead.
+sp_before_unnamed_byref         = ignore   # ignore/add/remove/force
+
+# Add or remove space after reference sign '&', if followed by a word.
+#
+# Overrides sp_type_func.
+sp_after_byref                  = ignore   # ignore/add/remove/force
+
+# Add or remove space after a reference sign '&', if followed by a function
+# prototype or function definition.
+#
+# Overrides sp_after_byref and sp_type_func.
+sp_after_byref_func             = ignore   # ignore/add/remove/force
+
+# Add or remove space before a reference sign '&', if followed by a function
+# prototype or function definition.
+sp_before_byref_func            = ignore   # ignore/add/remove/force
+
+# Add or remove space between type and word.
+#
+# Default: force
+sp_after_type                   = force    # ignore/add/remove/force
+
+# Add or remove space between 'decltype(...)' and word.
+sp_after_decltype               = ignore   # ignore/add/remove/force
+
+# (D) Add or remove space before the parenthesis in the D constructs
+# 'template Foo(' and 'class Foo('.
+sp_before_template_paren        = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'template' and '<'.
+# If set to ignore, sp_before_angle is used.
+sp_template_angle               = ignore   # ignore/add/remove/force
+
+# Add or remove space before '<'.
+sp_before_angle                 = ignore   # ignore/add/remove/force
+
+# Add or remove space inside '<' and '>'.
+sp_inside_angle                 = ignore   # ignore/add/remove/force
+
+# Add or remove space inside '<>'.
+sp_inside_angle_empty           = ignore   # ignore/add/remove/force
+
+# Add or remove space between '>' and ':'.
+sp_angle_colon                  = ignore   # ignore/add/remove/force
+
+# Add or remove space after '>'.
+sp_after_angle                  = ignore   # ignore/add/remove/force
+
+# Add or remove space between '>' and '(' as found in 'new List<byte>(foo);'.
+sp_angle_paren                  = ignore   # ignore/add/remove/force
+
+# Add or remove space between '>' and '()' as found in 'new List<byte>();'.
+sp_angle_paren_empty            = ignore   # ignore/add/remove/force
+
+# Add or remove space between '>' and a word as in 'List<byte> m;' or
+# 'template <typename T> static ...'.
+sp_angle_word                   = ignore   # ignore/add/remove/force
+
+# Add or remove space between '>' and '>' in '>>' (template stuff).
+#
+# Default: add
+sp_angle_shift                  = add      # ignore/add/remove/force
+
+# (C++11) Permit removal of the space between '>>' in 'foo<bar<int> >'. Note
+# that sp_angle_shift cannot remove the space without this option.
+sp_permit_cpp11_shift           = false    # true/false
+
+# Add or remove space before '(' of control statements ('if', 'for', 'switch',
+# 'while', etc.).
+sp_before_sparen                = force    # ignore/add/remove/force
+
+# Add or remove space inside '(' and ')' of control statements.
+sp_inside_sparen                = remove   # ignore/add/remove/force
+
+# Add or remove space after '(' of control statements.
+#
+# Overrides sp_inside_sparen.
+sp_inside_sparen_open           = ignore   # ignore/add/remove/force
+
+# Add or remove space before ')' of control statements.
+#
+# Overrides sp_inside_sparen.
+sp_inside_sparen_close          = ignore   # ignore/add/remove/force
+
+# Add or remove space after ')' of control statements.
+sp_after_sparen                 = ignore   # ignore/add/remove/force
+
+# Add or remove space between ')' and '{' of of control statements.
+sp_sparen_brace                 = force    # ignore/add/remove/force
+
+# (D) Add or remove space between 'invariant' and '('.
+sp_invariant_paren              = ignore   # ignore/add/remove/force
+
+# (D) Add or remove space after the ')' in 'invariant (C) c'.
+sp_after_invariant_paren        = ignore   # ignore/add/remove/force
+
+# Add or remove space before empty statement ';' on 'if', 'for' and 'while'.
+sp_special_semi                 = ignore   # ignore/add/remove/force
+
+# Add or remove space before ';'.
+#
+# Default: remove
+sp_before_semi                  = remove   # ignore/add/remove/force
+
+# Add or remove space before ';' in non-empty 'for' statements.
+sp_before_semi_for              = ignore   # ignore/add/remove/force
+
+# Add or remove space before a semicolon of an empty part of a for statement.
+sp_before_semi_for_empty        = ignore   # ignore/add/remove/force
+
+# Add or remove space after ';', except when followed by a comment.
+#
+# Default: add
+sp_after_semi                   = add      # ignore/add/remove/force
+
+# Add or remove space after ';' in non-empty 'for' statements.
+#
+# Default: force
+sp_after_semi_for               = force    # ignore/add/remove/force
+
+# Add or remove space after the final semicolon of an empty part of a for
+# statement, as in 'for ( ; ; <here> )'.
+sp_after_semi_for_empty         = ignore   # ignore/add/remove/force
+
+# Add or remove space before '[' (except '[]').
+sp_before_square                = ignore   # ignore/add/remove/force
+
+# Add or remove space before '[' for a variable definition.
+#
+# Default: remove
+sp_before_vardef_square         = remove   # ignore/add/remove/force
+
+# Add or remove space before '[' for asm block.
+sp_before_square_asm_block      = ignore   # ignore/add/remove/force
+
+# Add or remove space before '[]'.
+sp_before_squares               = ignore   # ignore/add/remove/force
+
+# Add or remove space before C++17 structured bindings.
+sp_cpp_before_struct_binding    = ignore   # ignore/add/remove/force
+
+# Add or remove space inside a non-empty '[' and ']'.
+sp_inside_square                = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space inside a non-empty Objective-C boxed array '@[' and
+# ']'. If set to ignore, sp_inside_square is used.
+sp_inside_square_oc_array       = ignore   # ignore/add/remove/force
+
+# Add or remove space after ',', i.e. 'a,b' vs. 'a, b'.
+sp_after_comma                  = ignore   # ignore/add/remove/force
+
+# Add or remove space before ','.
+#
+# Default: remove
+sp_before_comma                 = remove   # ignore/add/remove/force
+
+# (C#) Add or remove space between ',' and ']' in multidimensional array type
+# like 'int[,,]'.
+sp_after_mdatype_commas         = ignore   # ignore/add/remove/force
+
+# (C#) Add or remove space between '[' and ',' in multidimensional array type
+# like 'int[,,]'.
+sp_before_mdatype_commas        = ignore   # ignore/add/remove/force
+
+# (C#) Add or remove space between ',' in multidimensional array type
+# like 'int[,,]'.
+sp_between_mdatype_commas       = ignore   # ignore/add/remove/force
+
+# Add or remove space between an open parenthesis and comma,
+# i.e. '(,' vs. '( ,'.
+#
+# Default: force
+sp_paren_comma                  = force    # ignore/add/remove/force
+
+# Add or remove space before the variadic '...' when preceded by a
+# non-punctuator.
+sp_before_ellipsis              = ignore   # ignore/add/remove/force
+
+# Add or remove space between a type and '...'.
+sp_type_ellipsis                = ignore   # ignore/add/remove/force
+
+# (D) Add or remove space between a type and '?'.
+sp_type_question                = ignore   # ignore/add/remove/force
+
+# Add or remove space between ')' and '...'.
+sp_paren_ellipsis               = ignore   # ignore/add/remove/force
+
+# Add or remove space between ')' and a qualifier such as 'const'.
+sp_paren_qualifier              = ignore   # ignore/add/remove/force
+
+# Add or remove space between ')' and 'noexcept'.
+sp_paren_noexcept               = ignore   # ignore/add/remove/force
+
+# Add or remove space after class ':'.
+sp_after_class_colon            = ignore   # ignore/add/remove/force
+
+# Add or remove space before class ':'.
+sp_before_class_colon           = ignore   # ignore/add/remove/force
+
+# Add or remove space after class constructor ':'.
+sp_after_constr_colon           = ignore   # ignore/add/remove/force
+
+# Add or remove space before class constructor ':'.
+sp_before_constr_colon          = ignore   # ignore/add/remove/force
+
+# Add or remove space before case ':'.
+#
+# Default: remove
+sp_before_case_colon            = remove   # ignore/add/remove/force
+
+# Add or remove space between 'operator' and operator sign.
+sp_after_operator               = ignore   # ignore/add/remove/force
+
+# Add or remove space between the operator symbol and the open parenthesis, as
+# in 'operator ++('.
+sp_after_operator_sym           = ignore   # ignore/add/remove/force
+
+# Overrides sp_after_operator_sym when the operator has no arguments, as in
+# 'operator *()'.
+sp_after_operator_sym_empty     = ignore   # ignore/add/remove/force
+
+# Add or remove space after C/D cast, i.e. 'cast(int)a' vs. 'cast(int) a' or
+# '(int)a' vs. '(int) a'.
+sp_after_cast                   = remove   # ignore/add/remove/force
+
+# Add or remove spaces inside cast parentheses.
+sp_inside_paren_cast            = remove   # ignore/add/remove/force
+
+# Add or remove space between the type and open parenthesis in a C++ cast,
+# i.e. 'int(exp)' vs. 'int (exp)'.
+sp_cpp_cast_paren               = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'sizeof' and '('.
+sp_sizeof_paren                 = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'sizeof' and '...'.
+sp_sizeof_ellipsis              = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'sizeof...' and '('.
+sp_sizeof_ellipsis_paren        = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'decltype' and '('.
+sp_decltype_paren               = ignore   # ignore/add/remove/force
+
+# (Pawn) Add or remove space after the tag keyword.
+sp_after_tag                    = ignore   # ignore/add/remove/force
+
+# Add or remove space inside enum '{' and '}'.
+sp_inside_braces_enum           = ignore   # ignore/add/remove/force
+
+# Add or remove space inside struct/union '{' and '}'.
+sp_inside_braces_struct         = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space inside Objective-C boxed dictionary '{' and '}'
+sp_inside_braces_oc_dict        = ignore   # ignore/add/remove/force
+
+# Add or remove space after open brace in an unnamed temporary
+# direct-list-initialization.
+sp_after_type_brace_init_lst_open = ignore   # ignore/add/remove/force
+
+# Add or remove space before close brace in an unnamed temporary
+# direct-list-initialization.
+sp_before_type_brace_init_lst_close = ignore   # ignore/add/remove/force
+
+# Add or remove space inside an unnamed temporary direct-list-initialization.
+sp_inside_type_brace_init_lst   = ignore   # ignore/add/remove/force
+
+# Add or remove space inside '{' and '}'.
+sp_inside_braces                = ignore   # ignore/add/remove/force
+
+# Add or remove space inside '{}'.
+sp_inside_braces_empty          = ignore   # ignore/add/remove/force
+
+# Add or remove space around trailing return operator '->'.
+sp_trailing_return              = ignore   # ignore/add/remove/force
+
+# Add or remove space between return type and function name. A minimum of 1
+# is forced except for pointer return types.
+sp_type_func                    = ignore   # ignore/add/remove/force
+
+# Add or remove space between type and open brace of an unnamed temporary
+# direct-list-initialization.
+sp_type_brace_init_lst          = ignore   # ignore/add/remove/force
+
+# Add or remove space between function name and '(' on function declaration.
+sp_func_proto_paren             = remove   # ignore/add/remove/force
+
+# Add or remove space between function name and '()' on function declaration
+# without parameters.
+sp_func_proto_paren_empty       = remove   # ignore/add/remove/force
+
+# Add or remove space between function name and '(' with a typedef specifier.
+sp_func_type_paren              = remove   # ignore/add/remove/force
+
+# Add or remove space between alias name and '(' of a non-pointer function type typedef.
+sp_func_def_paren               = remove   # ignore/add/remove/force
+
+# Add or remove space between function name and '()' on function definition
+# without parameters.
+sp_func_def_paren_empty         = remove   # ignore/add/remove/force
+
+# Add or remove space inside empty function '()'.
+# Overrides sp_after_angle unless use_sp_after_angle_always is set to true.
+sp_inside_fparens               = remove   # ignore/add/remove/force
+
+# Add or remove space inside function '(' and ')'.
+sp_inside_fparen                = remove   # ignore/add/remove/force
+
+# Add or remove space inside the first parentheses in a function type, as in
+# 'void (*x)(...)'.
+sp_inside_tparen                = remove   # ignore/add/remove/force
+
+# Add or remove space between the ')' and '(' in a function type, as in
+# 'void (*x)(...)'.
+sp_after_tparen_close           = remove   # ignore/add/remove/force
+
+# Add or remove space between ']' and '(' when part of a function call.
+sp_square_fparen                = remove   # ignore/add/remove/force
+
+# Add or remove space between ')' and '{' of function.
+sp_fparen_brace                 = force    # ignore/add/remove/force
+
+# Add or remove space between ')' and '{' of s function call in object
+# initialization.
+#
+# Overrides sp_fparen_brace.
+sp_fparen_brace_initializer     = ignore   # ignore/add/remove/force
+
+# (Java) Add or remove space between ')' and '{{' of double brace initializer.
+sp_fparen_dbrace                = ignore   # ignore/add/remove/force
+
+# Add or remove space between function name and '(' on function calls.
+sp_func_call_paren              = remove   # ignore/add/remove/force
+
+# Add or remove space between function name and '()' on function calls without
+# parameters. If set to ignore (the default), sp_func_call_paren is used.
+sp_func_call_paren_empty        = remove   # ignore/add/remove/force
+
+# Add or remove space between the user function name and '(' on function
+# calls. You need to set a keyword to be a user function in the config file,
+# like:
+#   set func_call_user tr _ i18n
+sp_func_call_user_paren         = ignore   # ignore/add/remove/force
+
+# Add or remove space inside user function '(' and ')'.
+sp_func_call_user_inside_fparen = ignore   # ignore/add/remove/force
+
+# Add or remove space between nested parentheses with user functions,
+# i.e. '((' vs. '( ('.
+sp_func_call_user_paren_paren   = ignore   # ignore/add/remove/force
+
+# Add or remove space between a constructor/destructor and the open
+# parenthesis.
+sp_func_class_paren             = ignore   # ignore/add/remove/force
+
+# Add or remove space between a constructor without parameters or destructor
+# and '()'.
+sp_func_class_paren_empty       = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'return' and '('.
+sp_return_paren                 = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'return' and '{'.
+sp_return_brace                 = ignore   # ignore/add/remove/force
+
+# Add or remove space between '__attribute__' and '('.
+sp_attribute_paren              = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'defined' and '(' in '#if defined (FOO)'.
+sp_defined_paren                = remove   # ignore/add/remove/force
+
+# Add or remove space between 'throw' and '(' in 'throw (something)'.
+sp_throw_paren                  = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'throw' and anything other than '(' as in
+# '@throw [...];'.
+sp_after_throw                  = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'catch' and '(' in 'catch (something) { }'.
+# If set to ignore, sp_before_sparen is used.
+sp_catch_paren                  = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space between '@catch' and '('
+# in '@catch (something) { }'. If set to ignore, sp_catch_paren is used.
+sp_oc_catch_paren               = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space before Objective-C protocol list
+# as in '@protocol Protocol<here><Protocol_A>' or '@interface MyClass : NSObject<here><MyProtocol>'.
+sp_before_oc_proto_list         = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space between class name and '('
+# in '@interface className(categoryName)<ProtocolName>:BaseClass'
+sp_oc_classname_paren           = ignore   # ignore/add/remove/force
+
+# (D) Add or remove space between 'version' and '('
+# in 'version (something) { }'. If set to ignore, sp_before_sparen is used.
+sp_version_paren                = ignore   # ignore/add/remove/force
+
+# (D) Add or remove space between 'scope' and '('
+# in 'scope (something) { }'. If set to ignore, sp_before_sparen is used.
+sp_scope_paren                  = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'super' and '(' in 'super (something)'.
+#
+# Default: remove
+sp_super_paren                  = remove   # ignore/add/remove/force
+
+# Add or remove space between 'this' and '(' in 'this (something)'.
+#
+# Default: remove
+sp_this_paren                   = remove   # ignore/add/remove/force
+
+# Add or remove space between a macro name and its definition.
+sp_macro                        = ignore   # ignore/add/remove/force
+
+# Add or remove space between a macro function ')' and its definition.
+sp_macro_func                   = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'else' and '{' if on the same line.
+sp_else_brace                   = force    # ignore/add/remove/force
+
+# Add or remove space between '}' and 'else' if on the same line.
+sp_brace_else                   = force    # ignore/add/remove/force
+
+# Add or remove space between '}' and the name of a typedef on the same line.
+sp_brace_typedef                = ignore   # ignore/add/remove/force
+
+# Add or remove space before the '{' of a 'catch' statement, if the '{' and
+# 'catch' are on the same line, as in 'catch (decl) <here> {'.
+sp_catch_brace                  = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space before the '{' of a '@catch' statement, if the '{'
+# and '@catch' are on the same line, as in '@catch (decl) <here> {'.
+# If set to ignore, sp_catch_brace is used.
+sp_oc_catch_brace               = ignore   # ignore/add/remove/force
+
+# Add or remove space between '}' and 'catch' if on the same line.
+sp_brace_catch                  = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space between '}' and '@catch' if on the same line.
+# If set to ignore, sp_brace_catch is used.
+sp_oc_brace_catch               = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'finally' and '{' if on the same line.
+sp_finally_brace                = ignore   # ignore/add/remove/force
+
+# Add or remove space between '}' and 'finally' if on the same line.
+sp_brace_finally                = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'try' and '{' if on the same line.
+sp_try_brace                    = ignore   # ignore/add/remove/force
+
+# Add or remove space between get/set and '{' if on the same line.
+sp_getset_brace                 = ignore   # ignore/add/remove/force
+
+# Add or remove space between a variable and '{' for C++ uniform
+# initialization.
+sp_word_brace_init_lst          = ignore   # ignore/add/remove/force
+
+# Add or remove space between a variable and '{' for a namespace.
+#
+# Default: add
+sp_word_brace_ns                = add      # ignore/add/remove/force
+
+# Add or remove space before the '::' operator.
+sp_before_dc                    = ignore   # ignore/add/remove/force
+
+# Add or remove space after the '::' operator.
+sp_after_dc                     = ignore   # ignore/add/remove/force
+
+# (D) Add or remove around the D named array initializer ':' operator.
+sp_d_array_colon                = ignore   # ignore/add/remove/force
+
+# Add or remove space after the '!' (not) unary operator.
+#
+# Default: remove
+sp_not                          = remove   # ignore/add/remove/force
+
+# Add or remove space after the '~' (invert) unary operator.
+#
+# Default: remove
+sp_inv                          = remove   # ignore/add/remove/force
+
+# Add or remove space after the '&' (address-of) unary operator. This does not
+# affect the spacing after a '&' that is part of a type.
+#
+# Default: remove
+sp_addr                         = remove   # ignore/add/remove/force
+
+# Add or remove space around the '.' or '->' operators.
+#
+# Default: remove
+sp_member                       = remove   # ignore/add/remove/force
+
+# Add or remove space after the '*' (dereference) unary operator. This does
+# not affect the spacing after a '*' that is part of a type.
+#
+# Default: remove
+sp_deref                        = remove   # ignore/add/remove/force
+
+# Add or remove space after '+' or '-', as in 'x = -5' or 'y = +7'.
+#
+# Default: remove
+sp_sign                         = remove   # ignore/add/remove/force
+
+# Add or remove space between '++' and '--' the word to which it is being
+# applied, as in '(--x)' or 'y++;'.
+#
+# Default: remove
+sp_incdec                       = remove   # ignore/add/remove/force
+
+# Add or remove space before a backslash-newline at the end of a line.
+#
+# Default: add
+sp_before_nl_cont               = add      # ignore/add/remove/force
+
+# (OC) Add or remove space after the scope '+' or '-', as in '-(void) foo;'
+# or '+(int) bar;'.
+sp_after_oc_scope               = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space after the colon in message specs,
+# i.e. '-(int) f:(int) x;' vs. '-(int) f: (int) x;'.
+sp_after_oc_colon               = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space before the colon in message specs,
+# i.e. '-(int) f: (int) x;' vs. '-(int) f : (int) x;'.
+sp_before_oc_colon              = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space after the colon in immutable dictionary expression
+# 'NSDictionary *test = @{@"foo" :@"bar"};'.
+sp_after_oc_dict_colon          = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space before the colon in immutable dictionary expression
+# 'NSDictionary *test = @{@"foo" :@"bar"};'.
+sp_before_oc_dict_colon         = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space after the colon in message specs,
+# i.e. '[object setValue:1];' vs. '[object setValue: 1];'.
+sp_after_send_oc_colon          = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space before the colon in message specs,
+# i.e. '[object setValue:1];' vs. '[object setValue :1];'.
+sp_before_send_oc_colon         = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space after the (type) in message specs,
+# i.e. '-(int)f: (int) x;' vs. '-(int)f: (int)x;'.
+sp_after_oc_type                = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space after the first (type) in message specs,
+# i.e. '-(int) f:(int)x;' vs. '-(int)f:(int)x;'.
+sp_after_oc_return_type         = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space between '@selector' and '(',
+# i.e. '@selector(msgName)' vs. '@selector (msgName)'.
+# Also applies to '@protocol()' constructs.
+sp_after_oc_at_sel              = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space between '@selector(x)' and the following word,
+# i.e. '@selector(foo) a:' vs. '@selector(foo)a:'.
+sp_after_oc_at_sel_parens       = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space inside '@selector' parentheses,
+# i.e. '@selector(foo)' vs. '@selector( foo )'.
+# Also applies to '@protocol()' constructs.
+sp_inside_oc_at_sel_parens      = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space before a block pointer caret,
+# i.e. '^int (int arg){...}' vs. ' ^int (int arg){...}'.
+sp_before_oc_block_caret        = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space after a block pointer caret,
+# i.e. '^int (int arg){...}' vs. '^ int (int arg){...}'.
+sp_after_oc_block_caret         = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space between the receiver and selector in a message,
+# as in '[receiver selector ...]'.
+sp_after_oc_msg_receiver        = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space after '@property'.
+sp_after_oc_property            = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove space between '@synchronized' and the open parenthesis,
+# i.e. '@synchronized(foo)' vs. '@synchronized (foo)'.
+sp_after_oc_synchronized        = ignore   # ignore/add/remove/force
+
+# Add or remove space around the ':' in 'b ? t : f'.
+sp_cond_colon                   = ignore   # ignore/add/remove/force
+
+# Add or remove space before the ':' in 'b ? t : f'.
+#
+# Overrides sp_cond_colon.
+sp_cond_colon_before            = ignore   # ignore/add/remove/force
+
+# Add or remove space after the ':' in 'b ? t : f'.
+#
+# Overrides sp_cond_colon.
+sp_cond_colon_after             = ignore   # ignore/add/remove/force
+
+# Add or remove space around the '?' in 'b ? t : f'.
+sp_cond_question                = ignore   # ignore/add/remove/force
+
+# Add or remove space before the '?' in 'b ? t : f'.
+#
+# Overrides sp_cond_question.
+sp_cond_question_before         = ignore   # ignore/add/remove/force
+
+# Add or remove space after the '?' in 'b ? t : f'.
+#
+# Overrides sp_cond_question.
+sp_cond_question_after          = ignore   # ignore/add/remove/force
+
+# In the abbreviated ternary form '(a ?: b)', add or remove space between '?'
+# and ':'.
+#
+# Overrides all other sp_cond_* options.
+sp_cond_ternary_short           = ignore   # ignore/add/remove/force
+
+# Fix the spacing between 'case' and the label. Only 'ignore' and 'force' make
+# sense here.
+sp_case_label                   = ignore   # ignore/add/remove/force
+
+# (D) Add or remove space around the D '..' operator.
+sp_range                        = ignore   # ignore/add/remove/force
+
+# Add or remove space after ':' in a Java/C++11 range-based 'for',
+# as in 'for (Type var : expr)'.
+sp_after_for_colon              = ignore   # ignore/add/remove/force
+
+# Add or remove space before ':' in a Java/C++11 range-based 'for',
+# as in 'for (Type var : expr)'.
+sp_before_for_colon             = ignore   # ignore/add/remove/force
+
+# (D) Add or remove space between 'extern' and '(' as in 'extern (C)'.
+sp_extern_paren                 = ignore   # ignore/add/remove/force
+
+# Add or remove space after the opening of a C++ comment,
+# i.e. '// A' vs. '//A'.
+sp_cmt_cpp_start                = add      # ignore/add/remove/force
+
+# If true, space is added with sp_cmt_cpp_start will be added after doxygen
+# sequences like '///', '///<', '//!' and '//!<'.
+sp_cmt_cpp_doxygen              = false    # true/false
+
+# If true, space is added with sp_cmt_cpp_start will be added after Qt
+# translator or meta-data comments like '//:', '//=', and '//~'.
+sp_cmt_cpp_qttr                 = false    # true/false
+
+# Add or remove space between #else or #endif and a trailing comment.
+sp_endif_cmt                    = ignore   # ignore/add/remove/force
+
+# Add or remove space after 'new', 'delete' and 'delete[]'.
+sp_after_new                    = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'new' and '(' in 'new()'.
+sp_between_new_paren            = ignore   # ignore/add/remove/force
+
+# Add or remove space between ')' and type in 'new(foo) BAR'.
+sp_after_newop_paren            = ignore   # ignore/add/remove/force
+
+# Add or remove space inside parenthesis of the new operator
+# as in 'new(foo) BAR'.
+sp_inside_newop_paren           = ignore   # ignore/add/remove/force
+
+# Add or remove space after the open parenthesis of the new operator,
+# as in 'new(foo) BAR'.
+#
+# Overrides sp_inside_newop_paren.
+sp_inside_newop_paren_open      = ignore   # ignore/add/remove/force
+
+# Add or remove space before the close parenthesis of the new operator,
+# as in 'new(foo) BAR'.
+#
+# Overrides sp_inside_newop_paren.
+sp_inside_newop_paren_close     = ignore   # ignore/add/remove/force
+
+# Add or remove space before a trailing or embedded comment.
+sp_before_tr_emb_cmt            = ignore   # ignore/add/remove/force
+
+# Number of spaces before a trailing or embedded comment.
+sp_num_before_tr_emb_cmt        = 0        # unsigned number
+
+# (Java) Add or remove space between an annotation and the open parenthesis.
+sp_annotation_paren             = ignore   # ignore/add/remove/force
+
+# If true, vbrace tokens are dropped to the previous token and skipped.
+sp_skip_vbrace_tokens           = false    # true/false
+
+# Add or remove space after 'noexcept'.
+sp_after_noexcept               = ignore   # ignore/add/remove/force
+
+# Add or remove space after '_'.
+sp_vala_after_translation       = ignore   # ignore/add/remove/force
+
+# If true, a <TAB> is inserted after #define.
+force_tab_after_define          = false    # true/false
+
+#
+# Indenting options
+#
+
+# The number of columns to indent per level. Usually 2, 3, 4, or 8.
+#
+# Default: 8
+indent_columns                  = 4        # unsigned number
+
+# The continuation indent. If non-zero, this overrides the indent of '(', '['
+# and '=' continuation indents. Negative values are OK; negative value is
+# absolute and not increased for each '(' or '[' level.
+#
+# For FreeBSD, this is set to 4.
+indent_continue                 = 0        # number
+
+# The continuation indent, only for class header line(s). If non-zero, this
+# overrides the indent of 'class' continuation indents.
+indent_continue_class_head      = 0        # unsigned number
+
+# Whether to indent empty lines (i.e. lines which contain only spaces before
+# the newline character).
+indent_single_newlines          = false    # true/false
+
+# The continuation indent for func_*_param if they are true. If non-zero, this
+# overrides the indent.
+indent_param                    = 0        # unsigned number
+
+# How to use tabs when indenting code.
+#
+# 0: Spaces only
+# 1: Indent with tabs to brace level, align with spaces (default)
+# 2: Indent and align with tabs, using spaces when not on a tabstop
+#
+# Default: 1
+indent_with_tabs                = 0        # unsigned number
+
+# Whether to indent comments that are not at a brace level with tabs on a
+# tabstop. Requires indent_with_tabs=2. If false, will use spaces.
+indent_cmt_with_tabs            = false    # true/false
+
+# Whether to indent strings broken by '\' so that they line up.
+indent_align_string             = false    # true/false
+
+# The number of spaces to indent multi-line XML strings.
+# Requires indent_align_string=true.
+indent_xml_string               = 0        # unsigned number
+
+# Spaces to indent '{' from level.
+indent_brace                    = 0        # unsigned number
+
+# Whether braces are indented to the body level.
+indent_braces                   = false    # true/false
+
+# Whether to disable indenting function braces if indent_braces=true.
+indent_braces_no_func           = false    # true/false
+
+# Whether to disable indenting class braces if indent_braces=true.
+indent_braces_no_class          = false    # true/false
+
+# Whether to disable indenting struct braces if indent_braces=true.
+indent_braces_no_struct         = false    # true/false
+
+# Whether to indent based on the size of the brace parent,
+# i.e. 'if' => 3 spaces, 'for' => 4 spaces, etc.
+indent_brace_parent             = false    # true/false
+
+# Whether to indent based on the open parenthesis instead of the open brace
+# in '({\n'.
+indent_paren_open_brace         = false    # true/false
+
+# (C#) Whether to indent the brace of a C# delegate by another level.
+indent_cs_delegate_brace        = false    # true/false
+
+# (C#) Whether to indent a C# delegate (to handle delegates with no brace) by
+# another level.
+indent_cs_delegate_body         = false    # true/false
+
+# Whether to indent the body of a 'namespace'.
+indent_namespace                = false    # true/false
+
+# Whether to indent only the first namespace, and not any nested namespaces.
+# Requires indent_namespace=true.
+indent_namespace_single_indent  = false    # true/false
+
+# The number of spaces to indent a namespace block.
+# If set to zero, use the value indent_columns
+indent_namespace_level          = 0        # unsigned number
+
+# If the body of the namespace is longer than this number, it won't be
+# indented. Requires indent_namespace=true. 0 means no limit.
+indent_namespace_limit          = 0        # unsigned number
+
+# Whether the 'extern "C"' body is indented.
+indent_extern                   = false    # true/false
+
+# Whether the 'class' body is indented.
+indent_class                    = false    # true/false
+
+# Whether to indent the stuff after a leading base class colon.
+indent_class_colon              = false    # true/false
+
+# Whether to indent based on a class colon instead of the stuff after the
+# colon. Requires indent_class_colon=true.
+indent_class_on_colon           = false    # true/false
+
+# Whether to indent the stuff after a leading class initializer colon.
+indent_constr_colon             = false    # true/false
+
+# Virtual indent from the ':' for member initializers.
+#
+# Default: 2
+indent_ctor_init_leading        = 2        # unsigned number
+
+# Additional indent for constructor initializer list.
+# Negative values decrease indent down to the first column.
+indent_ctor_init                = 0        # number
+
+# Whether to indent 'if' following 'else' as a new block under the 'else'.
+# If false, 'else\nif' is treated as 'else if' for indenting purposes.
+indent_else_if                  = false    # true/false
+
+# Amount to indent variable declarations after a open brace.
+#
+#  <0: Relative
+# >=0: Absolute
+indent_var_def_blk              = 0        # number
+
+# Whether to indent continued variable declarations instead of aligning.
+indent_var_def_cont             = false    # true/false
+
+# Whether to indent continued shift expressions ('<<' and '>>') instead of
+# aligning. Set align_left_shift=false when enabling this.
+indent_shift                    = false    # true/false
+
+# Whether to force indentation of function definitions to start in column 1.
+indent_func_def_force_col1      = false    # true/false
+
+# Whether to indent continued function call parameters one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_func_call_param          = true     # true/false
+
+# Whether to indent continued function definition parameters one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_func_def_param           = true     # true/false
+
+# for function definitions, only if indent_func_def_param is false
+# Allows to align params when appropriate and indent them when not
+# behave as if it was true if paren position is more than this value
+# if paren position is more than the option value
+indent_func_def_param_paren_pos_threshold = 0        # unsigned number
+
+# Whether to indent continued function call prototype one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_func_proto_param         = true     # true/false
+
+# Whether to indent continued function call declaration one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_func_class_param         = true     # true/false
+
+# Whether to indent continued class variable constructors one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_func_ctor_var_param      = true     # true/false
+
+# Whether to indent continued template parameter list one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_template_param           = true     # true/false
+
+# Double the indent for indent_func_xxx_param options.
+# Use both values of the options indent_columns and indent_param.
+indent_func_param_double        = false    # true/false
+
+# Indentation column for standalone 'const' qualifier on a function
+# prototype.
+indent_func_const               = 0        # unsigned number
+
+# Indentation column for standalone 'throw' qualifier on a function
+# prototype.
+indent_func_throw               = 0        # unsigned number
+
+# How to indent within a macro followed by a brace on the same line
+# This allows reducing the indent in macros that have (for example)
+# `do { ... } while (0)` blocks bracketing them.
+#
+# true:  add an indent for the brace on the same line as the macro
+# false: do not add an indent for the brace on the same line as the macro
+#
+# Default: true
+indent_macro_brace              = true     # true/false
+
+# The number of spaces to indent a continued '->' or '.'.
+# Usually set to 0, 1, or indent_columns.
+indent_member                   = 0        # unsigned number
+
+# Whether lines broken at '.' or '->' should be indented by a single indent.
+# The indent_member option will not be effective if this is set to true.
+indent_member_single            = false    # true/false
+
+# Spaces to indent single line ('//') comments on lines before code.
+indent_sing_line_comments       = 0        # unsigned number
+
+# When opening a paren for a control statement (if, for, while, etc), increase
+# the indent level by this value. Negative values decrease the indent level.
+indent_sparen_extra             = 0        # number
+
+# Whether to indent trailing single line ('//') comments relative to the code
+# instead of trying to keep the same absolute column.
+indent_relative_single_line_comments = false    # true/false
+
+# Spaces to indent 'case' from 'switch'. Usually 0 or indent_columns.
+indent_switch_case              = indent_columns        # unsigned number
+
+# indent 'break' with 'case' from 'switch'.
+indent_switch_break_with_case   = false    # true/false
+
+# Whether to indent preprocessor statements inside of switch statements.
+#
+# Default: true
+indent_switch_pp                = true     # true/false
+
+# Spaces to shift the 'case' line, without affecting any other lines.
+# Usually 0.
+indent_case_shift               = 0        # unsigned number
+
+# Spaces to indent '{' from 'case'. By default, the brace will appear under
+# the 'c' in case. Usually set to 0 or indent_columns. Negative values are OK.
+indent_case_brace               = 0        # number
+
+# Whether to indent comments found in first column.
+indent_col1_comment             = false    # true/false
+
+# Whether to indent multi string literal in first column.
+indent_col1_multi_string_literal = false    # true/false
+
+# How to indent goto labels.
+#
+#  >0: Absolute column where 1 is the leftmost column
+# <=0: Subtract from brace indent
+#
+# Default: 1
+indent_label                    = -indent_columns # number
+
+# How to indent access specifiers that are followed by a
+# colon.
+#
+#  >0: Absolute column where 1 is the leftmost column
+# <=0: Subtract from brace indent
+#
+# Default: 1
+indent_access_spec              = 1        # number
+
+# Whether to indent the code after an access specifier by one level.
+# If true, this option forces 'indent_access_spec=0'.
+indent_access_spec_body         = false    # true/false
+
+# If an open parenthesis is followed by a newline, whether to indent the next
+# line so that it lines up after the open parenthesis (not recommended).
+indent_paren_nl                 = false    # true/false
+
+# How to indent a close parenthesis after a newline.
+#
+# 0: Indent to body level (default)
+# 1: Align under the open parenthesis
+# 2: Indent to the brace level
+indent_paren_close              = 0        # unsigned number
+
+# Whether to indent the open parenthesis of a function definition,
+# if the parenthesis is on its own line.
+indent_paren_after_func_def     = false    # true/false
+
+# Whether to indent the open parenthesis of a function declaration,
+# if the parenthesis is on its own line.
+indent_paren_after_func_decl    = false    # true/false
+
+# Whether to indent the open parenthesis of a function call,
+# if the parenthesis is on its own line.
+indent_paren_after_func_call    = false    # true/false
+
+# Whether to indent a comma when inside a parenthesis.
+# If true, aligns under the open parenthesis.
+indent_comma_paren              = false    # true/false
+
+# Whether to indent a Boolean operator when inside a parenthesis.
+# If true, aligns under the open parenthesis.
+indent_bool_paren               = false    # true/false
+
+# Whether to indent a semicolon when inside a for parenthesis.
+# If true, aligns under the open for parenthesis.
+indent_semicolon_for_paren      = false    # true/false
+
+# Whether to align the first expression to following ones
+# if indent_bool_paren=true.
+indent_first_bool_expr          = false    # true/false
+
+# Whether to align the first expression to following ones
+# if indent_semicolon_for_paren=true.
+indent_first_for_expr           = false    # true/false
+
+# If an open square is followed by a newline, whether to indent the next line
+# so that it lines up after the open square (not recommended).
+indent_square_nl                = false    # true/false
+
+# (ESQL/C) Whether to preserve the relative indent of 'EXEC SQL' bodies.
+indent_preserve_sql             = false    # true/false
+
+# Whether to align continued statements at the '='. If false or if the '=' is
+# followed by a newline, the next line is indent one tab.
+#
+# Default: true
+indent_align_assign             = false    # true/false
+
+# If true, the indentation of the chunks after a '=' sequence will be set at
+# LHS token indentation column before '='.
+indent_off_after_assign         = false    # true/false
+
+# Whether to align continued statements at the '('. If false or the '(' is
+# followed by a newline, the next line indent is one tab.
+#
+# Default: true
+indent_align_paren              = false    # true/false
+
+# (OC) Whether to indent Objective-C code inside message selectors.
+indent_oc_inside_msg_sel        = false    # true/false
+
+# (OC) Whether to indent Objective-C blocks at brace level instead of usual
+# rules.
+indent_oc_block                 = false    # true/false
+
+# (OC) Indent for Objective-C blocks in a message relative to the parameter
+# name.
+#
+# =0: Use indent_oc_block rules
+# >0: Use specified number of spaces to indent
+indent_oc_block_msg             = 0        # unsigned number
+
+# (OC) Minimum indent for subsequent parameters
+indent_oc_msg_colon             = 0        # unsigned number
+
+# (OC) Whether to prioritize aligning with initial colon (and stripping spaces
+# from lines, if necessary).
+#
+# Default: true
+indent_oc_msg_prioritize_first_colon = true     # true/false
+
+# (OC) Whether to indent blocks the way that Xcode does by default
+# (from the keyword if the parameter is on its own line; otherwise, from the
+# previous indentation level). Requires indent_oc_block_msg=true.
+indent_oc_block_msg_xcode_style = false    # true/false
+
+# (OC) Whether to indent blocks from where the brace is, relative to a
+# message keyword. Requires indent_oc_block_msg=true.
+indent_oc_block_msg_from_keyword = false    # true/false
+
+# (OC) Whether to indent blocks from where the brace is, relative to a message
+# colon. Requires indent_oc_block_msg=true.
+indent_oc_block_msg_from_colon  = false    # true/false
+
+# (OC) Whether to indent blocks from where the block caret is.
+# Requires indent_oc_block_msg=true.
+indent_oc_block_msg_from_caret  = false    # true/false
+
+# (OC) Whether to indent blocks from where the brace caret is.
+# Requires indent_oc_block_msg=true.
+indent_oc_block_msg_from_brace  = false    # true/false
+
+# When indenting after virtual brace open and newline add further spaces to
+# reach this minimum indent.
+indent_min_vbrace_open          = 0        # unsigned number
+
+# Whether to add further spaces after regular indent to reach next tabstop
+# when identing after virtual brace open and newline.
+indent_vbrace_open_on_tabstop   = false    # true/false
+
+# How to indent after a brace followed by another token (not a newline).
+# true:  indent all contained lines to match the token
+# false: indent all contained lines to match the brace
+#
+# Default: true
+indent_token_after_brace        = true     # true/false
+
+# Whether to indent the body of a C++11 lambda.
+indent_cpp_lambda_body          = false    # true/false
+
+# How to indent compound literals that are being returned.
+# true: add both the indent from return & the compound literal open brace (ie:
+#       2 indent levels)
+# false: only indent 1 level, don't add the indent for the open brace, only add
+#        the indent for the return.
+#
+# Default: true
+indent_compound_literal_return  = true     # true/false
+
+# (C#) Whether to indent a 'using' block if no braces are used.
+#
+# Default: true
+indent_using_block              = true     # true/false
+
+# How to indent the continuation of ternary operator.
+#
+# 0: Off (default)
+# 1: When the `if_false` is a continuation, indent it under `if_false`
+# 2: When the `:` is a continuation, indent it under `?`
+indent_ternary_operator         = 0        # unsigned number
+
+# Whether to indent the statments inside ternary operator.
+indent_inside_ternary_operator  = false    # true/false
+
+# If true, the indentation of the chunks after a `return` sequence will be set at return indentation column.
+indent_off_after_return         = false    # true/false
+
+# If true, the indentation of the chunks after a `return new` sequence will be set at return indentation column.
+indent_off_after_return_new     = false    # true/false
+
+# If true, the tokens after return are indented with regular single indentation. By default (false) the indentation is after the return token.
+indent_single_after_return      = false    # true/false
+
+# Whether to ignore indent and alignment for 'asm' blocks (i.e. assume they
+# have their own indentation).
+indent_ignore_asm_block         = false    # true/false
+
+#
+# Newline adding and removing options
+#
+
+# Whether to collapse empty blocks between '{' and '}'.
+nl_collapse_empty_body          = false    # true/false
+
+# Don't split one-line braced assignments, as in 'foo_t f = { 1, 2 };'.
+nl_assign_leave_one_liners      = false    # true/false
+
+# Don't split one-line braced statements inside a 'class xx { }' body.
+nl_class_leave_one_liners       = false    # true/false
+
+# Don't split one-line enums, as in 'enum foo { BAR = 15 };'
+nl_enum_leave_one_liners        = false    # true/false
+
+# Don't split one-line get or set functions.
+nl_getset_leave_one_liners      = false    # true/false
+
+# (C#) Don't split one-line property get or set functions.
+nl_cs_property_leave_one_liners = false    # true/false
+
+# Don't split one-line function definitions, as in 'int foo() { return 0; }'.
+# might modify nl_func_type_name
+nl_func_leave_one_liners        = false    # true/false
+
+# Don't split one-line C++11 lambdas, as in '[]() { return 0; }'.
+nl_cpp_lambda_leave_one_liners  = false    # true/false
+
+# Don't split one-line if/else statements, as in 'if(...) b++;'.
+nl_if_leave_one_liners          = false    # true/false
+
+# Don't split one-line while statements, as in 'while(...) b++;'.
+nl_while_leave_one_liners       = false    # true/false
+
+# Don't split one-line for statements, as in 'for(...) b++;'.
+nl_for_leave_one_liners         = false    # true/false
+
+# (OC) Don't split one-line Objective-C messages.
+nl_oc_msg_leave_one_liner       = false    # true/false
+
+# (OC) Add or remove newline between method declaration and '{'.
+nl_oc_mdef_brace                = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove newline between Objective-C block signature and '{'.
+nl_oc_block_brace               = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove blank line before '@interface' statement.
+nl_oc_before_interface          = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove blank line before '@implementation' statement.
+nl_oc_before_implementation     = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove blank line before '@end' statement.
+nl_oc_before_end                = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove newline between '@interface' and '{'.
+nl_oc_interface_brace           = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove newline between '@implementation' and '{'.
+nl_oc_implementation_brace      = ignore   # ignore/add/remove/force
+
+# Add or remove newlines at the start of the file.
+nl_start_of_file                = ignore   # ignore/add/remove/force
+
+# The minimum number of newlines at the start of the file (only used if
+# nl_start_of_file is 'add' or 'force').
+nl_start_of_file_min            = 0        # unsigned number
+
+# Add or remove newline at the end of the file.
+nl_end_of_file                  = ignore   # ignore/add/remove/force
+
+# The minimum number of newlines at the end of the file (only used if
+# nl_end_of_file is 'add' or 'force').
+nl_end_of_file_min              = 0        # unsigned number
+
+# Add or remove newline between '=' and '{'.
+nl_assign_brace                 = ignore   # ignore/add/remove/force
+
+# (D) Add or remove newline between '=' and '['.
+nl_assign_square                = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '[]' and '{'.
+nl_tsquare_brace                = ignore   # ignore/add/remove/force
+
+# (D) Add or remove newline after '= ['. Will also affect the newline before
+# the ']'.
+nl_after_square_assign          = ignore   # ignore/add/remove/force
+
+# Add or remove newline between a function call's ')' and '{', as in
+# 'list_for_each(item, &list) { }'.
+nl_fcall_brace                  = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'enum' and '{'.
+nl_enum_brace                   = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'enum' and 'class'.
+nl_enum_class                   = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'enum class' and the identifier.
+nl_enum_class_identifier        = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'enum class' type and ':'.
+nl_enum_identifier_colon        = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'enum class identifier :' and type.
+nl_enum_colon_type              = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'struct and '{'.
+nl_struct_brace                 = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'union' and '{'.
+nl_union_brace                  = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'if' and '{'.
+nl_if_brace                     = remove   # ignore/add/remove/force
+
+# Add or remove newline between '}' and 'else'.
+nl_brace_else                   = remove   # ignore/add/remove/force
+
+# Add or remove newline between 'else if' and '{'. If set to ignore,
+# nl_if_brace is used instead.
+nl_elseif_brace                 = remove   # ignore/add/remove/force
+
+# Add or remove newline between 'else' and '{'.
+nl_else_brace                   = remove   # ignore/add/remove/force
+
+# Add or remove newline between 'else' and 'if'.
+nl_else_if                      = ignore   # ignore/add/remove/force
+
+# Add or remove newline before '{' opening brace
+nl_before_opening_brace_func_class_def = ignore   # ignore/add/remove/force
+
+# Add or remove newline before 'if'/'else if' closing parenthesis.
+nl_before_if_closing_paren      = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '}' and 'finally'.
+nl_brace_finally                = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'finally' and '{'.
+nl_finally_brace                = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'try' and '{'.
+nl_try_brace                    = ignore   # ignore/add/remove/force
+
+# Add or remove newline between get/set and '{'.
+nl_getset_brace                 = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'for' and '{'.
+nl_for_brace                    = ignore   # ignore/add/remove/force
+
+# Add or remove newline before the '{' of a 'catch' statement, as in
+# 'catch (decl) <here> {'.
+nl_catch_brace                  = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove newline before the '{' of a '@catch' statement, as in
+# '@catch (decl) <here> {'. If set to ignore, nl_catch_brace is used.
+nl_oc_catch_brace               = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '}' and 'catch'.
+nl_brace_catch                  = ignore   # ignore/add/remove/force
+
+# (OC) Add or remove newline between '}' and '@catch'. If set to ignore,
+# nl_brace_catch is used.
+nl_oc_brace_catch               = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '}' and ']'.
+nl_brace_square                 = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '}' and ')' in a function invocation.
+nl_brace_fparen                 = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'while' and '{'.
+nl_while_brace                  = remove   # ignore/add/remove/force
+
+# (D) Add or remove newline between 'scope (x)' and '{'.
+nl_scope_brace                  = ignore   # ignore/add/remove/force
+
+# (D) Add or remove newline between 'unittest' and '{'.
+nl_unittest_brace               = ignore   # ignore/add/remove/force
+
+# (D) Add or remove newline between 'version (x)' and '{'.
+nl_version_brace                = ignore   # ignore/add/remove/force
+
+# (C#) Add or remove newline between 'using' and '{'.
+nl_using_brace                  = ignore   # ignore/add/remove/force
+
+# Add or remove newline between two open or close braces. Due to general
+# newline/brace handling, REMOVE may not work.
+nl_brace_brace                  = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'do' and '{'.
+nl_do_brace                     = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '}' and 'while' of 'do' statement.
+nl_brace_while                  = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'switch' and '{'.
+nl_switch_brace                 = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'synchronized' and '{'.
+nl_synchronized_brace           = ignore   # ignore/add/remove/force
+
+# Add a newline between ')' and '{' if the ')' is on a different line than the
+# if/for/etc.
+#
+# Overrides nl_for_brace, nl_if_brace, nl_switch_brace, nl_while_switch and
+# nl_catch_brace.
+nl_multi_line_cond              = false    # true/false
+
+# Add a newline after '(' if an if/for/while/switch condition spans multiple
+# lines
+nl_multi_line_sparen_open       = ignore   # ignore/add/remove/force
+
+# Add a newline before ')' if an if/for/while/switch condition spans multiple
+# lines. Overrides nl_before_if_closing_paren if both are specified.
+nl_multi_line_sparen_close      = ignore   # ignore/add/remove/force
+
+# Force a newline in a define after the macro name for multi-line defines.
+nl_multi_line_define            = false    # true/false
+
+# Whether to add a newline before 'case', and a blank line before a 'case'
+# statement that follows a ';' or '}'.
+nl_before_case                  = false    # true/false
+
+# Whether to add a newline after a 'case' statement.
+nl_after_case                   = true     # true/false
+
+# Add or remove newline between a case ':' and '{'.
+#
+# Overrides nl_after_case.
+nl_case_colon_brace             = remove   # ignore/add/remove/force
+
+# Add or remove newline between ')' and 'throw'.
+nl_before_throw                 = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'namespace' and '{'.
+nl_namespace_brace              = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<...>' of a template class.
+nl_template_class               = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<...>' of a template class declaration.
+#
+# Overrides nl_template_class.
+nl_template_class_decl          = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<>' of a specialized class declaration.
+#
+# Overrides nl_template_class_decl.
+nl_template_class_decl_special  = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<...>' of a template class definition.
+#
+# Overrides nl_template_class.
+nl_template_class_def           = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<>' of a specialized class definition.
+#
+# Overrides nl_template_class_def.
+nl_template_class_def_special   = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<...>' of a template function.
+nl_template_func                = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<...>' of a template function
+# declaration.
+#
+# Overrides nl_template_func.
+nl_template_func_decl           = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<>' of a specialized function
+# declaration.
+#
+# Overrides nl_template_func_decl.
+nl_template_func_decl_special   = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<...>' of a template function
+# definition.
+#
+# Overrides nl_template_func.
+nl_template_func_def            = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<>' of a specialized function
+# definition.
+#
+# Overrides nl_template_func_def.
+nl_template_func_def_special    = ignore   # ignore/add/remove/force
+
+# Add or remove newline after 'template<...>' of a template variable.
+nl_template_var                 = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'template<...>' and 'using' of a templated
+# type alias.
+nl_template_using               = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'class' and '{'.
+nl_class_brace                  = ignore   # ignore/add/remove/force
+
+# Add or remove newline before or after (depending on pos_class_comma,
+# may not be IGNORE) each',' in the base class list.
+nl_class_init_args              = ignore   # ignore/add/remove/force
+
+# Add or remove newline after each ',' in the constructor member
+# initialization. Related to nl_constr_colon, pos_constr_colon and
+# pos_constr_comma.
+nl_constr_init_args             = ignore   # ignore/add/remove/force
+
+# Add or remove newline before first element, after comma, and after last
+# element, in 'enum'.
+nl_enum_own_lines               = ignore   # ignore/add/remove/force
+
+# Add or remove newline between return type and function name in a function
+# definition.
+# might be modified by nl_func_leave_one_liners
+nl_func_type_name               = ignore   # ignore/add/remove/force
+
+# Add or remove newline between return type and function name inside a class
+# definition. If set to ignore, nl_func_type_name or nl_func_proto_type_name
+# is used instead.
+nl_func_type_name_class         = ignore   # ignore/add/remove/force
+
+# Add or remove newline between class specification and '::'
+# in 'void A::f() { }'. Only appears in separate member implementation (does
+# not appear with in-line implementation).
+nl_func_class_scope             = ignore   # ignore/add/remove/force
+
+# Add or remove newline between function scope and name, as in
+# 'void A :: <here> f() { }'.
+nl_func_scope_name              = ignore   # ignore/add/remove/force
+
+# Add or remove newline between return type and function name in a prototype.
+nl_func_proto_type_name         = ignore   # ignore/add/remove/force
+
+# Add or remove newline between a function name and the opening '(' in the
+# declaration.
+nl_func_paren                   = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_paren for functions with no parameters.
+nl_func_paren_empty             = ignore   # ignore/add/remove/force
+
+# Add or remove newline between a function name and the opening '(' in the
+# definition.
+nl_func_def_paren               = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_def_paren for functions with no parameters.
+nl_func_def_paren_empty         = ignore   # ignore/add/remove/force
+
+# Add or remove newline between a function name and the opening '(' in the
+# call.
+nl_func_call_paren              = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_call_paren for functions with no parameters.
+nl_func_call_paren_empty        = ignore   # ignore/add/remove/force
+
+# Add or remove newline after '(' in a function declaration.
+nl_func_decl_start              = ignore   # ignore/add/remove/force
+
+# Add or remove newline after '(' in a function definition.
+nl_func_def_start               = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_decl_start when there is only one parameter.
+nl_func_decl_start_single       = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_def_start when there is only one parameter.
+nl_func_def_start_single        = ignore   # ignore/add/remove/force
+
+# Whether to add a newline after '(' in a function declaration if '(' and ')'
+# are in different lines. If false, nl_func_decl_start is used instead.
+nl_func_decl_start_multi_line   = false    # true/false
+
+# Whether to add a newline after '(' in a function definition if '(' and ')'
+# are in different lines. If false, nl_func_def_start is used instead.
+nl_func_def_start_multi_line    = false    # true/false
+
+# Add or remove newline after each ',' in a function declaration.
+nl_func_decl_args               = ignore   # ignore/add/remove/force
+
+# Add or remove newline after each ',' in a function definition.
+nl_func_def_args                = ignore   # ignore/add/remove/force
+
+# Add or remove newline after each ',' in a function call.
+nl_func_call_args               = ignore   # ignore/add/remove/force
+
+# Whether to add a newline after each ',' in a function declaration if '('
+# and ')' are in different lines. If false, nl_func_decl_args is used instead.
+nl_func_decl_args_multi_line    = false    # true/false
+
+# Whether to add a newline after each ',' in a function definition if '('
+# and ')' are in different lines. If false, nl_func_def_args is used instead.
+nl_func_def_args_multi_line     = false    # true/false
+
+# Add or remove newline before the ')' in a function declaration.
+nl_func_decl_end                = ignore   # ignore/add/remove/force
+
+# Add or remove newline before the ')' in a function definition.
+nl_func_def_end                 = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_decl_end when there is only one parameter.
+nl_func_decl_end_single         = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_def_end when there is only one parameter.
+nl_func_def_end_single          = ignore   # ignore/add/remove/force
+
+# Whether to add a newline before ')' in a function declaration if '(' and ')'
+# are in different lines. If false, nl_func_decl_end is used instead.
+nl_func_decl_end_multi_line     = false    # true/false
+
+# Whether to add a newline before ')' in a function definition if '(' and ')'
+# are in different lines. If false, nl_func_def_end is used instead.
+nl_func_def_end_multi_line      = false    # true/false
+
+# Add or remove newline between '()' in a function declaration.
+nl_func_decl_empty              = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '()' in a function definition.
+nl_func_def_empty               = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '()' in a function call.
+nl_func_call_empty              = ignore   # ignore/add/remove/force
+
+# Whether to add a newline after '(' in a function call,
+# has preference over nl_func_call_start_multi_line.
+nl_func_call_start              = ignore   # ignore/add/remove/force
+
+# Whether to add a newline before ')' in a function call.
+nl_func_call_end                = ignore   # ignore/add/remove/force
+
+# Whether to add a newline after '(' in a function call if '(' and ')' are in
+# different lines.
+nl_func_call_start_multi_line   = false    # true/false
+
+# Whether to add a newline after each ',' in a function call if '(' and ')'
+# are in different lines.
+nl_func_call_args_multi_line    = false    # true/false
+
+# Whether to add a newline before ')' in a function call if '(' and ')' are in
+# different lines.
+nl_func_call_end_multi_line     = false    # true/false
+
+# Whether to respect nl_func_call_XXX option incase of closure args.
+nl_func_call_args_multi_line_ignore_closures = false    # true/false
+
+# Whether to add a newline after '<' of a template parameter list.
+nl_template_start               = false    # true/false
+
+# Whether to add a newline after each ',' in a template parameter list.
+nl_template_args                = false    # true/false
+
+# Whether to add a newline before '>' of a template parameter list.
+nl_template_end                 = false    # true/false
+
+# (OC) Whether to put each Objective-C message parameter on a separate line.
+# See nl_oc_msg_leave_one_liner.
+nl_oc_msg_args                  = false    # true/false
+
+# Add or remove newline between function signature and '{'.
+nl_fdef_brace                   = remove   # ignore/add/remove/force
+
+# Add or remove newline between function signature and '{',
+# if signature ends with ')'. Overrides nl_fdef_brace.
+nl_fdef_brace_cond              = ignore   # ignore/add/remove/force
+
+# Add or remove newline between C++11 lambda signature and '{'.
+nl_cpp_ldef_brace               = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'return' and the return expression.
+nl_return_expr                  = ignore   # ignore/add/remove/force
+
+# Whether to add a newline after semicolons, except in 'for' statements.
+nl_after_semicolon              = true     # true/false
+
+# (Java) Add or remove newline between the ')' and '{{' of the double brace
+# initializer.
+nl_paren_dbrace_open            = ignore   # ignore/add/remove/force
+
+# Whether to add a newline after the type in an unnamed temporary
+# direct-list-initialization.
+nl_type_brace_init_lst          = ignore   # ignore/add/remove/force
+
+# Whether to add a newline after the open brace in an unnamed temporary
+# direct-list-initialization.
+nl_type_brace_init_lst_open     = ignore   # ignore/add/remove/force
+
+# Whether to add a newline before the close brace in an unnamed temporary
+# direct-list-initialization.
+nl_type_brace_init_lst_close    = ignore   # ignore/add/remove/force
+
+# Whether to add a newline after '{'. This also adds a newline before the
+# matching '}'.
+nl_after_brace_open             = false    # true/false
+
+# Whether to add a newline between the open brace and a trailing single-line
+# comment. Requires nl_after_brace_open=true.
+nl_after_brace_open_cmt         = false    # true/false
+
+# Whether to add a newline after a virtual brace open with a non-empty body.
+# These occur in un-braced if/while/do/for statement bodies.
+nl_after_vbrace_open            = false    # true/false
+
+# Whether to add a newline after a virtual brace open with an empty body.
+# These occur in un-braced if/while/do/for statement bodies.
+nl_after_vbrace_open_empty      = false    # true/false
+
+# Whether to add a newline after '}'. Does not apply if followed by a
+# necessary ';'.
+nl_after_brace_close            = false    # true/false
+
+# Whether to add a newline after a virtual brace close,
+# as in 'if (foo) a++; <here> return;'.
+nl_after_vbrace_close           = false    # true/false
+
+# Add or remove newline between the close brace and identifier,
+# as in 'struct { int a; } <here> b;'. Affects enumerations, unions and
+# structures. If set to ignore, uses nl_after_brace_close.
+nl_brace_struct_var             = ignore   # ignore/add/remove/force
+
+# Whether to alter newlines in '#define' macros.
+nl_define_macro                 = false    # true/false
+
+# Whether to alter newlines between consecutive parenthesis closes. The number
+# of closing parentheses in a line will depend on respective open parenthesis
+# lines.
+nl_squeeze_paren_close          = false    # true/false
+
+# Whether to remove blanks after '#ifxx' and '#elxx', or before '#elxx' and
+# '#endif'. Does not affect top-level #ifdefs.
+nl_squeeze_ifdef                = false    # true/false
+
+# Makes the nl_squeeze_ifdef option affect the top-level #ifdefs as well.
+nl_squeeze_ifdef_top_level      = false    # true/false
+
+# Add or remove blank line before 'if'.
+nl_before_if                    = ignore   # ignore/add/remove/force
+
+# Add or remove blank line after 'if' statement. Add/Force work only if the
+# next token is not a closing brace.
+nl_after_if                     = ignore   # ignore/add/remove/force
+
+# Add or remove blank line before 'for'.
+nl_before_for                   = ignore   # ignore/add/remove/force
+
+# Add or remove blank line after 'for' statement.
+nl_after_for                    = ignore   # ignore/add/remove/force
+
+# Add or remove blank line before 'while'.
+nl_before_while                 = ignore   # ignore/add/remove/force
+
+# Add or remove blank line after 'while' statement.
+nl_after_while                  = ignore   # ignore/add/remove/force
+
+# Add or remove blank line before 'switch'.
+nl_before_switch                = ignore   # ignore/add/remove/force
+
+# Add or remove blank line after 'switch' statement.
+nl_after_switch                 = ignore   # ignore/add/remove/force
+
+# Add or remove blank line before 'synchronized'.
+nl_before_synchronized          = ignore   # ignore/add/remove/force
+
+# Add or remove blank line after 'synchronized' statement.
+nl_after_synchronized           = ignore   # ignore/add/remove/force
+
+# Add or remove blank line before 'do'.
+nl_before_do                    = ignore   # ignore/add/remove/force
+
+# Add or remove blank line after 'do/while' statement.
+nl_after_do                     = ignore   # ignore/add/remove/force
+
+# Whether to put a blank line before 'return' statements, unless after an open
+# brace.
+nl_before_return                = false    # true/false
+
+# Whether to put a blank line after 'return' statements, unless followed by a
+# close brace.
+nl_after_return                 = false    # true/false
+
+# Whether to put a blank line before a member '.' or '->' operators.
+nl_before_member                = ignore   # ignore/add/remove/force
+
+# (Java) Whether to put a blank line after a member '.' or '->' operators.
+nl_after_member                 = ignore   # ignore/add/remove/force
+
+# Whether to double-space commented-entries in 'struct'/'union'/'enum'.
+nl_ds_struct_enum_cmt           = false    # true/false
+
+# Whether to force a newline before '}' of a 'struct'/'union'/'enum'.
+# (Lower priority than eat_blanks_before_close_brace.)
+nl_ds_struct_enum_close_brace   = false    # true/false
+
+# Add or remove newline before or after (depending on pos_class_colon) a class
+# colon, as in 'class Foo <here> : <or here> public Bar'.
+nl_class_colon                  = ignore   # ignore/add/remove/force
+
+# Add or remove newline around a class constructor colon. The exact position
+# depends on nl_constr_init_args, pos_constr_colon and pos_constr_comma.
+nl_constr_colon                 = ignore   # ignore/add/remove/force
+
+# Whether to collapse a two-line namespace, like 'namespace foo\n{ decl; }'
+# into a single line. If true, prevents other brace newline rules from turning
+# such code into four lines.
+nl_namespace_two_to_one_liner   = false    # true/false
+
+# Whether to remove a newline in simple unbraced if statements, turning them
+# into one-liners, as in 'if(b)\n i++;' => 'if(b) i++;'.
+nl_create_if_one_liner          = false    # true/false
+
+# Whether to remove a newline in simple unbraced for statements, turning them
+# into one-liners, as in 'for (...)\n stmt;' => 'for (...) stmt;'.
+nl_create_for_one_liner         = false    # true/false
+
+# Whether to remove a newline in simple unbraced while statements, turning
+# them into one-liners, as in 'while (expr)\n stmt;' => 'while (expr) stmt;'.
+nl_create_while_one_liner       = false    # true/false
+
+# Whether to collapse a function definition whose body (not counting braces)
+# is only one line so that the entire definition (prototype, braces, body) is
+# a single line.
+nl_create_func_def_one_liner    = false    # true/false
+
+# Whether to collapse a function definition whose body (not counting braces)
+# is only one line so that the entire definition (prototype, braces, body) is
+# a single line.
+nl_create_list_one_liner        = false    # true/false
+
+# Whether to split one-line simple unbraced if statements into two lines by
+# adding a newline, as in 'if(b) <here> i++;'.
+nl_split_if_one_liner           = true     # true/false
+
+# Whether to split one-line simple unbraced for statements into two lines by
+# adding a newline, as in 'for (...) <here> stmt;'.
+nl_split_for_one_liner          = true     # true/false
+
+# Whether to split one-line simple unbraced while statements into two lines by
+# adding a newline, as in 'while (expr) <here> stmt;'.
+nl_split_while_one_liner        = true     # true/false
+
+#
+# Blank line options
+#
+
+# The maximum number of consecutive newlines (3 = 2 blank lines).
+nl_max                          = 0        # unsigned number
+
+# The maximum number of consecutive newlines in a function.
+nl_max_blank_in_func            = 0        # unsigned number
+
+# The number of newlines before a function prototype.
+nl_before_func_body_proto       = 0        # unsigned number
+
+# The number of newlines before a multi-line function definition.
+nl_before_func_body_def         = 0        # unsigned number
+
+# The number of newlines before a class constructor/destructor prototype.
+nl_before_func_class_proto      = 0        # unsigned number
+
+# The number of newlines before a class constructor/destructor definition.
+nl_before_func_class_def        = 0        # unsigned number
+
+# The number of newlines after a function prototype.
+nl_after_func_proto             = 0        # unsigned number
+
+# The number of newlines after a function prototype, if not followed by
+# another function prototype.
+nl_after_func_proto_group       = 0        # unsigned number
+
+# The number of newlines after a class constructor/destructor prototype.
+nl_after_func_class_proto       = 0        # unsigned number
+
+# The number of newlines after a class constructor/destructor prototype,
+# if not followed by another constructor/destructor prototype.
+nl_after_func_class_proto_group = 0        # unsigned number
+
+# Whether one-line method definitions inside a class body should be treated
+# as if they were prototypes for the purposes of adding newlines.
+#
+# Requires nl_class_leave_one_liners=true. Overrides nl_before_func_body_def
+# and nl_before_func_class_def for one-liners.
+nl_class_leave_one_liner_groups = false    # true/false
+
+# The number of newlines after '}' of a multi-line function body.
+nl_after_func_body              = 0        # unsigned number
+
+# The number of newlines after '}' of a multi-line function body in a class
+# declaration. Also affects class constructors/destructors.
+#
+# Overrides nl_after_func_body.
+nl_after_func_body_class        = 0        # unsigned number
+
+# The number of newlines after '}' of a single line function body. Also
+# affects class constructors/destructors.
+#
+# Overrides nl_after_func_body and nl_after_func_body_class.
+nl_after_func_body_one_liner    = 0        # unsigned number
+
+# The number of blank lines after a block of variable definitions at the top
+# of a function body.
+#
+# 0: No change (default).
+nl_func_var_def_blk             = 0        # unsigned number
+
+# The number of newlines before a block of typedefs. If nl_after_access_spec
+# is non-zero, that option takes precedence.
+#
+# 0: No change (default).
+nl_typedef_blk_start            = 0        # unsigned number
+
+# The number of newlines after a block of typedefs.
+#
+# 0: No change (default).
+nl_typedef_blk_end              = 0        # unsigned number
+
+# The maximum number of consecutive newlines within a block of typedefs.
+#
+# 0: No change (default).
+nl_typedef_blk_in               = 0        # unsigned number
+
+# The number of newlines before a block of variable definitions not at the top
+# of a function body. If nl_after_access_spec is non-zero, that option takes
+# precedence.
+#
+# 0: No change (default).
+nl_var_def_blk_start            = 0        # unsigned number
+
+# The number of newlines after a block of variable definitions not at the top
+# of a function body.
+#
+# 0: No change (default).
+nl_var_def_blk_end              = 0        # unsigned number
+
+# The maximum number of consecutive newlines within a block of variable
+# definitions.
+#
+# 0: No change (default).
+nl_var_def_blk_in               = 0        # unsigned number
+
+# The minimum number of newlines before a multi-line comment.
+# Doesn't apply if after a brace open or another multi-line comment.
+nl_before_block_comment         = 0        # unsigned number
+
+# The minimum number of newlines before a single-line C comment.
+# Doesn't apply if after a brace open or other single-line C comments.
+nl_before_c_comment             = 0        # unsigned number
+
+# The minimum number of newlines before a CPP comment.
+# Doesn't apply if after a brace open or other CPP comments.
+nl_before_cpp_comment           = 0        # unsigned number
+
+# Whether to force a newline after a multi-line comment.
+nl_after_multiline_comment      = false    # true/false
+
+# Whether to force a newline after a label's colon.
+nl_after_label_colon            = false    # true/false
+
+# The number of newlines after '}' or ';' of a struct/enum/union definition.
+nl_after_struct                 = 0        # unsigned number
+
+# The number of newlines before a class definition.
+nl_before_class                 = 0        # unsigned number
+
+# The number of newlines after '}' or ';' of a class definition.
+nl_after_class                  = 0        # unsigned number
+
+# The number of newlines before a namespace.
+nl_before_namespace             = 0        # unsigned number
+
+# The number of newlines after '{' of a namespace. This also adds newlines
+# before the matching '}'.
+#
+# 0: Apply eat_blanks_after_open_brace or eat_blanks_before_close_brace if
+#     applicable, otherwise no change.
+#
+# Overrides eat_blanks_after_open_brace and eat_blanks_before_close_brace.
+nl_inside_namespace             = 0        # unsigned number
+
+# The number of newlines after '}' of a namespace.
+nl_after_namespace              = 0        # unsigned number
+
+# The number of newlines before an access specifier label. This also includes
+# the Qt-specific 'signals:' and 'slots:'. Will not change the newline count
+# if after a brace open.
+#
+# 0: No change (default).
+nl_before_access_spec           = 0        # unsigned number
+
+# The number of newlines after an access specifier label. This also includes
+# the Qt-specific 'signals:' and 'slots:'. Will not change the newline count
+# if after a brace open.
+#
+# 0: No change (default).
+#
+# Overrides nl_typedef_blk_start and nl_var_def_blk_start.
+nl_after_access_spec            = 0        # unsigned number
+
+# The number of newlines between a function definition and the function
+# comment, as in '// comment\n <here> void foo() {...}'.
+#
+# 0: No change (default).
+nl_comment_func_def             = 0        # unsigned number
+
+# The number of newlines after a try-catch-finally block that isn't followed
+# by a brace close.
+#
+# 0: No change (default).
+nl_after_try_catch_finally      = 0        # unsigned number
+
+# (C#) The number of newlines before and after a property, indexer or event
+# declaration.
+#
+# 0: No change (default).
+nl_around_cs_property           = 0        # unsigned number
+
+# (C#) The number of newlines between the get/set/add/remove handlers.
+#
+# 0: No change (default).
+nl_between_get_set              = 0        # unsigned number
+
+# (C#) Add or remove newline between property and the '{'.
+nl_property_brace               = ignore   # ignore/add/remove/force
+
+# Whether to remove blank lines after '{'.
+eat_blanks_after_open_brace     = false    # true/false
+
+# Whether to remove blank lines before '}'.
+eat_blanks_before_close_brace   = false    # true/false
+
+# How aggressively to remove extra newlines not in preprocessor.
+#
+# 0: No change (default)
+# 1: Remove most newlines not handled by other config
+# 2: Remove all newlines and reformat completely by config
+nl_remove_extra_newlines        = 0        # unsigned number
+
+# (Java) Add or remove newline after an annotation statement. Only affects
+# annotations that are after a newline.
+nl_after_annotation             = ignore   # ignore/add/remove/force
+
+# (Java) Add or remove newline between two annotations.
+nl_between_annotation           = ignore   # ignore/add/remove/force
+
+# The number of newlines before a whole-file #ifdef.
+#
+# 0: No change (default).
+nl_before_whole_file_ifdef      = 0        # unsigned number
+
+# The number of newlines after a whole-file #ifdef.
+#
+# 0: No change (default).
+nl_after_whole_file_ifdef       = 0        # unsigned number
+
+# The number of newlines before a whole-file #endif.
+#
+# 0: No change (default).
+nl_before_whole_file_endif      = 0        # unsigned number
+
+# The number of newlines after a whole-file #endif.
+#
+# 0: No change (default).
+nl_after_whole_file_endif       = 0        # unsigned number
+
+#
+# Positioning options
+#
+
+# The position of arithmetic operators in wrapped expressions.
+pos_arith                       = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of assignment in wrapped expressions. Do not affect '='
+# followed by '{'.
+pos_assign                      = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of Boolean operators in wrapped expressions.
+pos_bool                        = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of comparison operators in wrapped expressions.
+pos_compare                     = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of conditional operators, as in the '?' and ':' of
+# 'expr ? stmt : stmt', in wrapped expressions.
+pos_conditional                 = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of the comma in wrapped expressions.
+pos_comma                       = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of the comma in enum entries.
+pos_enum_comma                  = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of the comma in the base class list if there is more than one
+# line. Affects nl_class_init_args.
+pos_class_comma                 = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of the comma in the constructor initialization list.
+# Related to nl_constr_colon, nl_constr_init_args and pos_constr_colon.
+pos_constr_comma                = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of trailing/leading class colon, between class and base class
+# list. Affects nl_class_colon.
+pos_class_colon                 = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of colons between constructor and member initialization.
+# Related to nl_constr_colon, nl_constr_init_args and pos_constr_comma.
+pos_constr_colon                = ignore   # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+#
+# Line splitting options
+#
+
+# Try to limit code width to N columns.
+code_width                      = 0        # unsigned number
+
+# Whether to fully split long 'for' statements at semi-colons.
+ls_for_split_full               = false    # true/false
+
+# Whether to fully split long function prototypes/calls at commas.
+# The option ls_code_width has priority over the option ls_func_split_full.
+ls_func_split_full              = false    # true/false
+
+# Whether to split lines as close to code_width as possible and ignore some
+# groupings.
+# The option ls_code_width has priority over the option ls_func_split_full.
+ls_code_width                   = false    # true/false
+
+#
+# Code alignment options (not left column spaces/tabs)
+#
+
+# Whether to keep non-indenting tabs.
+align_keep_tabs                 = false    # true/false
+
+# Whether to use tabs for aligning.
+align_with_tabs                 = false    # true/false
+
+# Whether to bump out to the next tab when aligning.
+align_on_tabstop                = false    # true/false
+
+# Whether to right-align numbers.
+align_number_right              = false    # true/false
+
+# Whether to keep whitespace not required for alignment.
+align_keep_extra_space          = false    # true/false
+
+# Whether to align variable definitions in prototypes and functions.
+align_func_params               = false    # true/false
+
+# The span for aligning parameter definitions in function on parameter name.
+#
+# 0: Don't align (default).
+align_func_params_span          = 0        # unsigned number
+
+# The threshold for aligning function parameter definitions.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_func_params_thresh        = 0        # number
+
+# The gap for aligning function parameter definitions.
+align_func_params_gap           = 0        # unsigned number
+
+# The span for aligning constructor value.
+#
+# 0: Don't align (default).
+align_constr_value_span         = 0        # unsigned number
+
+# The threshold for aligning constructor value.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_constr_value_thresh       = 0        # number
+
+# The gap for aligning constructor value.
+align_constr_value_gap          = 0        # unsigned number
+
+# Whether to align parameters in single-line functions that have the same
+# name. The function names must already be aligned with each other.
+align_same_func_call_params     = false    # true/false
+
+# The span for aligning function-call parameters for single line functions.
+#
+# 0: Don't align (default).
+align_same_func_call_params_span = 0        # unsigned number
+
+# The threshold for aligning function-call parameters for single line
+# functions.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_same_func_call_params_thresh = 0        # number
+
+# The span for aligning variable definitions.
+#
+# 0: Don't align (default).
+align_var_def_span              = 0        # unsigned number
+
+# How to consider (or treat) the '*' in the alignment of variable definitions.
+#
+# 0: Part of the type     'void *   foo;' (default)
+# 1: Part of the variable 'void     *foo;'
+# 2: Dangling             'void    *foo;'
+# Dangling: the '*' will not be taken into account when aligning.
+align_var_def_star_style        = 0        # unsigned number
+
+# How to consider (or treat) the '&' in the alignment of variable definitions.
+#
+# 0: Part of the type     'long &   foo;' (default)
+# 1: Part of the variable 'long     &foo;'
+# 2: Dangling             'long    &foo;'
+# Dangling: the '&' will not be taken into account when aligning.
+align_var_def_amp_style         = 0        # unsigned number
+
+# The threshold for aligning variable definitions.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_var_def_thresh            = 0        # number
+
+# The gap for aligning variable definitions.
+align_var_def_gap               = 0        # unsigned number
+
+# Whether to align the colon in struct bit fields.
+align_var_def_colon             = false    # true/false
+
+# The gap for aligning the colon in struct bit fields.
+align_var_def_colon_gap         = 0        # unsigned number
+
+# Whether to align any attribute after the variable name.
+align_var_def_attribute         = false    # true/false
+
+# Whether to align inline struct/enum/union variable definitions.
+align_var_def_inline            = false    # true/false
+
+# The span for aligning on '=' in assignments.
+#
+# 0: Don't align (default).
+align_assign_span               = 0        # unsigned number
+
+# The span for aligning on '=' in function prototype modifier.
+#
+# 0: Don't align (default).
+align_assign_func_proto_span    = 0        # unsigned number
+
+# The threshold for aligning on '=' in assignments.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_assign_thresh             = 0        # number
+
+# How to apply align_assign_span to function declaration "assignments", i.e.
+# 'virtual void foo() = 0' or '~foo() = {default|delete}'.
+#
+# 0: Align with other assignments (default)
+# 1: Align with each other, ignoring regular assignments
+# 2: Don't align
+align_assign_decl_func          = 0        # unsigned number
+
+# The span for aligning on '=' in enums.
+#
+# 0: Don't align (default).
+align_enum_equ_span             = 0        # unsigned number
+
+# The threshold for aligning on '=' in enums.
+# Use a negative number for absolute thresholds.
+#
+# 0: no limit (default).
+align_enum_equ_thresh           = 0        # number
+
+# The span for aligning class member definitions.
+#
+# 0: Don't align (default).
+align_var_class_span            = 0        # unsigned number
+
+# The threshold for aligning class member definitions.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_var_class_thresh          = 0        # number
+
+# The gap for aligning class member definitions.
+align_var_class_gap             = 0        # unsigned number
+
+# The span for aligning struct/union member definitions.
+#
+# 0: Don't align (default).
+align_var_struct_span           = 0        # unsigned number
+
+# The threshold for aligning struct/union member definitions.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_var_struct_thresh         = 0        # number
+
+# The gap for aligning struct/union member definitions.
+align_var_struct_gap            = 0        # unsigned number
+
+# The span for aligning struct initializer values.
+#
+# 0: Don't align (default).
+align_struct_init_span          = 0        # unsigned number
+
+# The span for aligning single-line typedefs.
+#
+# 0: Don't align (default).
+align_typedef_span              = 0        # unsigned number
+
+# The minimum space between the type and the synonym of a typedef.
+align_typedef_gap               = 0        # unsigned number
+
+# How to align typedef'd functions with other typedefs.
+#
+# 0: Don't mix them at all (default)
+# 1: Align the open parenthesis with the types
+# 2: Align the function type name with the other type names
+align_typedef_func              = 0        # unsigned number
+
+# How to consider (or treat) the '*' in the alignment of typedefs.
+#
+# 0: Part of the typedef type, 'typedef int * pint;' (default)
+# 1: Part of type name:        'typedef int   *pint;'
+# 2: Dangling:                 'typedef int  *pint;'
+# Dangling: the '*' will not be taken into account when aligning.
+align_typedef_star_style        = 0        # unsigned number
+
+# How to consider (or treat) the '&' in the alignment of typedefs.
+#
+# 0: Part of the typedef type, 'typedef int & intref;' (default)
+# 1: Part of type name:        'typedef int   &intref;'
+# 2: Dangling:                 'typedef int  &intref;'
+# Dangling: the '&' will not be taken into account when aligning.
+align_typedef_amp_style         = 0        # unsigned number
+
+# The span for aligning comments that end lines.
+#
+# 0: Don't align (default).
+align_right_cmt_span            = 0        # unsigned number
+
+# Minimum number of columns between preceding text and a trailing comment in
+# order for the comment to qualify for being aligned. Must be non-zero to have
+# an effect.
+align_right_cmt_gap             = 0        # unsigned number
+
+# If aligning comments, whether to mix with comments after '}' and #endif with
+# less than three spaces before the comment.
+align_right_cmt_mix             = false    # true/false
+
+# Whether to only align trailing comments that are at the same brace level.
+align_right_cmt_same_level      = false    # true/false
+
+# Minimum column at which to align trailing comments. Comments which are
+# aligned beyond this column, but which can be aligned in a lesser column,
+# may be "pulled in".
+#
+# 0: Ignore (default).
+align_right_cmt_at_col          = 0        # unsigned number
+
+# The span for aligning function prototypes.
+#
+# 0: Don't align (default).
+align_func_proto_span           = 0        # unsigned number
+
+# The threshold for aligning function prototypes.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_func_proto_thresh         = 0        # number
+
+# Minimum gap between the return type and the function name.
+align_func_proto_gap            = 0        # unsigned number
+
+# Whether to align function prototypes on the 'operator' keyword instead of
+# what follows.
+align_on_operator               = false    # true/false
+
+# Whether to mix aligning prototype and variable declarations. If true,
+# align_var_def_XXX options are used instead of align_func_proto_XXX options.
+align_mix_var_proto             = false    # true/false
+
+# Whether to align single-line functions with function prototypes.
+# Uses align_func_proto_span.
+align_single_line_func          = false    # true/false
+
+# Whether to align the open brace of single-line functions.
+# Requires align_single_line_func=true. Uses align_func_proto_span.
+align_single_line_brace         = false    # true/false
+
+# Gap for align_single_line_brace.
+align_single_line_brace_gap     = 0        # unsigned number
+
+# (OC) The span for aligning Objective-C message specifications.
+#
+# 0: Don't align (default).
+align_oc_msg_spec_span          = 0        # unsigned number
+
+# Whether to align macros wrapped with a backslash and a newline. This will
+# not work right if the macro contains a multi-line comment.
+align_nl_cont                   = false    # true/false
+
+# Whether to align macro functions and variables together.
+align_pp_define_together        = false    # true/false
+
+# The span for aligning on '#define' bodies.
+#
+# =0: Don't align (default)
+# >0: Number of lines (including comments) between blocks
+align_pp_define_span            = 0        # unsigned number
+
+# The minimum space between label and value of a preprocessor define.
+align_pp_define_gap             = 0        # unsigned number
+
+# Whether to align lines that start with '<<' with previous '<<'.
+#
+# Default: true
+align_left_shift                = true     # true/false
+
+# Whether to align text after 'asm volatile ()' colons.
+align_asm_colon                 = false    # true/false
+
+# (OC) Span for aligning parameters in an Objective-C message call
+# on the ':'.
+#
+# 0: Don't align.
+align_oc_msg_colon_span         = 0        # unsigned number
+
+# (OC) Whether to always align with the first parameter, even if it is too
+# short.
+align_oc_msg_colon_first        = false    # true/false
+
+# (OC) Whether to align parameters in an Objective-C '+' or '-' declaration
+# on the ':'.
+align_oc_decl_colon             = false    # true/false
+
+# (OC) Whether to not align parameters in an Objectve-C message call if first
+# colon is not on next line of the message call (the same way Xcode does
+# aligment)
+align_oc_msg_colon_xcode_like   = false    # true/false
+
+#
+# Comment modification options
+#
+
+# Try to wrap comments at N columns.
+cmt_width                       = 0        # unsigned number
+
+# How to reflow comments.
+#
+# 0: No reflowing (apart from the line wrapping due to cmt_width) (default)
+# 1: No touching at all
+# 2: Full reflow
+cmt_reflow_mode                 = 1        # unsigned number
+
+# Whether to convert all tabs to spaces in comments. If false, tabs in
+# comments are left alone, unless used for indenting.
+cmt_convert_tab_to_spaces       = false    # true/false
+
+# Whether to apply changes to multi-line comments, including cmt_width,
+# keyword substitution and leading chars.
+#
+# Default: true
+cmt_indent_multi                = false    # true/false
+
+# Whether to group c-comments that look like they are in a block.
+cmt_c_group                     = false    # true/false
+
+# Whether to put an empty '/*' on the first line of the combined c-comment.
+cmt_c_nl_start                  = false    # true/false
+
+# Whether to add a newline before the closing '*/' of the combined c-comment.
+cmt_c_nl_end                    = false    # true/false
+
+# Whether to change cpp-comments into c-comments.
+cmt_cpp_to_c                    = false    # true/false
+
+# Whether to group cpp-comments that look like they are in a block. Only
+# meaningful if cmt_cpp_to_c=true.
+cmt_cpp_group                   = false    # true/false
+
+# Whether to put an empty '/*' on the first line of the combined cpp-comment
+# when converting to a c-comment.
+#
+# Requires cmt_cpp_to_c=true and cmt_cpp_group=true.
+cmt_cpp_nl_start                = false    # true/false
+
+# Whether to add a newline before the closing '*/' of the combined cpp-comment
+# when converting to a c-comment.
+#
+# Requires cmt_cpp_to_c=true and cmt_cpp_group=true.
+cmt_cpp_nl_end                  = false    # true/false
+
+# Whether to put a star on subsequent comment lines.
+cmt_star_cont                   = false    # true/false
+
+# The number of spaces to insert at the start of subsequent comment lines.
+cmt_sp_before_star_cont         = 0        # unsigned number
+
+# The number of spaces to insert after the star on subsequent comment lines.
+cmt_sp_after_star_cont          = 0        # unsigned number
+
+# For multi-line comments with a '*' lead, remove leading spaces if the first
+# and last lines of the comment are the same length.
+#
+# Default: true
+cmt_multi_check_last            = true     # true/false
+
+# For multi-line comments with a '*' lead, remove leading spaces if the first
+# and last lines of the comment are the same length AND if the length is
+# bigger as the first_len minimum.
+#
+# Default: 4
+cmt_multi_first_len_minimum     = 4        # unsigned number
+
+# Path to a file that contains text to insert at the beginning of a file if
+# the file doesn't start with a C/C++ comment. If the inserted text contains
+# '$(filename)', that will be replaced with the current file's name.
+cmt_insert_file_header          = ""         # string
+
+# Path to a file that contains text to insert at the end of a file if the
+# file doesn't end with a C/C++ comment. If the inserted text contains
+# '$(filename)', that will be replaced with the current file's name.
+cmt_insert_file_footer          = ""         # string
+
+# Path to a file that contains text to insert before a function definition if
+# the function isn't preceded by a C/C++ comment. If the inserted text
+# contains '$(function)', '$(javaparam)' or '$(fclass)', these will be
+# replaced with, respectively, the name of the function, the javadoc '@param'
+# and '@return' stuff, or the name of the class to which the member function
+# belongs.
+cmt_insert_func_header          = ""         # string
+
+# Path to a file that contains text to insert before a class if the class
+# isn't preceded by a C/C++ comment. If the inserted text contains '$(class)',
+# that will be replaced with the class name.
+cmt_insert_class_header         = ""         # string
+
+# Path to a file that contains text to insert before an Objective-C message
+# specification, if the method isn't preceded by a C/C++ comment. If the
+# inserted text contains '$(message)' or '$(javaparam)', these will be
+# replaced with, respectively, the name of the function, or the javadoc
+# '@param' and '@return' stuff.
+cmt_insert_oc_msg_header        = ""         # string
+
+# Whether a comment should be inserted if a preprocessor is encountered when
+# stepping backwards from a function name.
+#
+# Applies to cmt_insert_oc_msg_header, cmt_insert_func_header and
+# cmt_insert_class_header.
+cmt_insert_before_preproc       = false    # true/false
+
+# Whether a comment should be inserted if a function is declared inline to a
+# class definition.
+#
+# Applies to cmt_insert_func_header.
+#
+# Default: true
+cmt_insert_before_inlines       = true     # true/false
+
+# Whether a comment should be inserted if the function is a class constructor
+# or destructor.
+#
+# Applies to cmt_insert_func_header.
+cmt_insert_before_ctor_dtor     = false    # true/false
+
+#
+# Code modifying options (non-whitespace)
+#
+
+# Add or remove braces on a single-line 'do' statement.
+mod_full_brace_do               = force    # ignore/add/remove/force
+
+# Add or remove braces on a single-line 'for' statement.
+mod_full_brace_for              = force    # ignore/add/remove/force
+
+# (Pawn) Add or remove braces on a single-line function definition.
+mod_full_brace_function         = force    # ignore/add/remove/force
+
+# Add or remove braces on a single-line 'if' statement. Braces will not be
+# removed if the braced statement contains an 'else'.
+mod_full_brace_if               = force    # ignore/add/remove/force
+
+# Whether to enforce that all blocks of an 'if'/'else if'/'else' chain either
+# have, or do not have, braces. If true, braces will be added if any block
+# needs braces, and will only be removed if they can be removed from all
+# blocks.
+#
+# Overrides mod_full_brace_if.
+mod_full_brace_if_chain         = false    # true/false
+
+# Whether to add braces to all blocks of an 'if'/'else if'/'else' chain.
+# If true, mod_full_brace_if_chain will only remove braces from an 'if' that
+# does not have an 'else if' or 'else'.
+mod_full_brace_if_chain_only    = false    # true/false
+
+# Add or remove braces on single-line 'while' statement.
+mod_full_brace_while            = force    # ignore/add/remove/force
+
+# Add or remove braces on single-line 'using ()' statement.
+mod_full_brace_using            = ignore   # ignore/add/remove/force
+
+# Don't remove braces around statements that span N newlines
+mod_full_brace_nl               = 0        # unsigned number
+
+# Whether to prevent removal of braces from 'if'/'for'/'while'/etc. blocks
+# which span multiple lines.
+#
+# Affects:
+#   mod_full_brace_for
+#   mod_full_brace_if
+#   mod_full_brace_if_chain
+#   mod_full_brace_if_chain_only
+#   mod_full_brace_while
+#   mod_full_brace_using
+#
+# Does not affect:
+#   mod_full_brace_do
+#   mod_full_brace_function
+mod_full_brace_nl_block_rem_mlcond = false    # true/false
+
+# Add or remove unnecessary parenthesis on 'return' statement.
+mod_paren_on_return             = remove   # ignore/add/remove/force
+
+# (Pawn) Whether to change optional semicolons to real semicolons.
+mod_pawn_semicolon              = false    # true/false
+
+# Whether to fully parenthesize Boolean expressions in 'while' and 'if'
+# statement, as in 'if (a && b > c)' => 'if (a && (b > c))'.
+mod_full_paren_if_bool          = false    # true/false
+
+# Whether to remove superfluous semicolons.
+mod_remove_extra_semicolon      = false    # true/false
+
+# If a function body exceeds the specified number of newlines and doesn't have
+# a comment after the close brace, a comment will be added.
+mod_add_long_function_closebrace_comment = 0        # unsigned number
+
+# If a namespace body exceeds the specified number of newlines and doesn't
+# have a comment after the close brace, a comment will be added.
+mod_add_long_namespace_closebrace_comment = 0        # unsigned number
+
+# If a class body exceeds the specified number of newlines and doesn't have a
+# comment after the close brace, a comment will be added.
+mod_add_long_class_closebrace_comment = 0        # unsigned number
+
+# If a switch body exceeds the specified number of newlines and doesn't have a
+# comment after the close brace, a comment will be added.
+mod_add_long_switch_closebrace_comment = 0        # unsigned number
+
+# If an #ifdef body exceeds the specified number of newlines and doesn't have
+# a comment after the #endif, a comment will be added.
+mod_add_long_ifdef_endif_comment = 0        # unsigned number
+
+# If an #ifdef or #else body exceeds the specified number of newlines and
+# doesn't have a comment after the #else, a comment will be added.
+mod_add_long_ifdef_else_comment = 0        # unsigned number
+
+# Whether to take care of the case by the mod_sort_xx options.
+mod_sort_case_sensitive         = false    # true/false
+
+# Whether to sort consecutive single-line 'import' statements.
+mod_sort_import                 = false    # true/false
+
+# (C#) Whether to sort consecutive single-line 'using' statements.
+mod_sort_using                  = false    # true/false
+
+# Whether to sort consecutive single-line '#include' statements (C/C++) and
+# '#import' statements (Objective-C). Be aware that this has the potential to
+# break your code if your includes/imports have ordering dependencies.
+mod_sort_include                = false    # true/false
+
+# Whether to prioritize '#include' and '#import' statements that contain
+# filename without extension when sorting is enabled.
+mod_sort_incl_import_prioritize_filename = false    # true/false
+
+# Whether to prioritize '#include' and '#import' statements that does not
+# contain extensions when sorting is enabled.
+mod_sort_incl_import_prioritize_extensionless = false    # true/false
+
+# Whether to prioritize '#include' and '#import' statements that contain
+# angle over quotes when sorting is enabled.
+mod_sort_incl_import_prioritize_angle_over_quotes = false    # true/false
+
+# Whether to ignore file extension in '#include' and '#import' statements
+# for sorting comparison.
+mod_sort_incl_import_ignore_extension = false    # true/false
+
+# Whether to group '#include' and '#import' statements when sorting is enabled.
+mod_sort_incl_import_grouping_enabled = false    # true/false
+
+# Whether to move a 'break' that appears after a fully braced 'case' before
+# the close brace, as in 'case X: { ... } break;' => 'case X: { ... break; }'.
+mod_move_case_break             = false    # true/false
+
+# Add or remove braces around a fully braced case statement. Will only remove
+# braces if there are no variable declarations in the block.
+mod_case_brace                  = ignore   # ignore/add/remove/force
+
+# Whether to remove a void 'return;' that appears as the last statement in a
+# function.
+mod_remove_empty_return         = false    # true/false
+
+# Add or remove the comma after the last value of an enumeration.
+mod_enum_last_comma             = ignore   # ignore/add/remove/force
+
+# (OC) Whether to organize the properties. If true, properties will be
+# rearranged according to the mod_sort_oc_property_*_weight factors.
+mod_sort_oc_properties          = false    # true/false
+
+# (OC) Weight of a class property modifier.
+mod_sort_oc_property_class_weight = 0        # number
+
+# (OC) Weight of 'atomic' and 'nonatomic'.
+mod_sort_oc_property_thread_safe_weight = 0        # number
+
+# (OC) Weight of 'readwrite' when organizing properties.
+mod_sort_oc_property_readwrite_weight = 0        # number
+
+# (OC) Weight of a reference type specifier ('retain', 'copy', 'assign',
+# 'weak', 'strong') when organizing properties.
+mod_sort_oc_property_reference_weight = 0        # number
+
+# (OC) Weight of getter type ('getter=') when organizing properties.
+mod_sort_oc_property_getter_weight = 0        # number
+
+# (OC) Weight of setter type ('setter=') when organizing properties.
+mod_sort_oc_property_setter_weight = 0        # number
+
+# (OC) Weight of nullability type ('nullable', 'nonnull', 'null_unspecified',
+# 'null_resettable') when organizing properties.
+mod_sort_oc_property_nullability_weight = 0        # number
+
+#
+# Preprocessor options
+#
+
+# Add or remove indentation of preprocessor directives inside #if blocks
+# at brace level 0 (file-level).
+pp_indent                       = ignore   # ignore/add/remove/force
+
+# Whether to indent #if/#else/#endif at the brace level. If false, these are
+# indented from column 1.
+pp_indent_at_level              = true     # true/false
+
+# Specifies the number of columns to indent preprocessors per level
+# at brace level 0 (file-level). If pp_indent_at_level=false, also specifies
+# the number of columns to indent preprocessors per level
+# at brace level > 0 (function-level).
+#
+# Default: 1
+pp_indent_count                 = 1        # unsigned number
+
+# Add or remove space after # based on pp_level of #if blocks.
+pp_space                        = remove   # ignore/add/remove/force
+
+# Sets the number of spaces per level added with pp_space.
+pp_space_count                  = 0        # unsigned number
+
+# The indent for '#region' and '#endregion' in C# and '#pragma region' in
+# C/C++. Negative values decrease indent down to the first column.
+pp_indent_region                = 0        # number
+
+# Whether to indent the code between #region and #endregion.
+pp_region_indent_code           = false    # true/false
+
+# If pp_indent_at_level=true, sets the indent for #if, #else and #endif when
+# not at file-level. Negative values decrease indent down to the first column.
+#
+# =0: Indent preprocessors using output_tab_size
+# >0: Column at which all preprocessors will be indented
+pp_indent_if                    = 0        # number
+
+# Whether to indent the code between #if, #else and #endif.
+pp_if_indent_code               = false    # true/false
+
+# Whether to indent '#define' at the brace level. If false, these are
+# indented from column 1.
+pp_define_at_level              = false    # true/false
+
+# Whether to ignore the '#define' body while formatting.
+pp_ignore_define_body           = false    # true/false
+
+# Whether to indent case statements between #if, #else, and #endif.
+# Only applies to the indent of the preprocesser that the case statements
+# directly inside of.
+#
+# Default: true
+pp_indent_case                  = true     # true/false
+
+# Whether to indent whole function definitions between #if, #else, and #endif.
+# Only applies to the indent of the preprocesser that the function definition
+# is directly inside of.
+#
+# Default: true
+pp_indent_func_def              = true     # true/false
+
+# Whether to indent extern C blocks between #if, #else, and #endif.
+# Only applies to the indent of the preprocesser that the extern block is
+# directly inside of.
+#
+# Default: true
+pp_indent_extern                = true     # true/false
+
+# Whether to indent braces directly inside #if, #else, and #endif.
+# Only applies to the indent of the preprocesser that the braces are directly
+# inside of.
+#
+# Default: true
+pp_indent_brace                 = true     # true/false
+
+#
+# Sort includes options
+#
+
+# The regex for include category with priority 0.
+include_category_0              = ""         # string
+
+# The regex for include category with priority 1.
+include_category_1              = ""         # string
+
+# The regex for include category with priority 2.
+include_category_2              = ""         # string
+
+#
+# Use or Do not Use options
+#
+
+# true:  indent_func_call_param will be used (default)
+# false: indent_func_call_param will NOT be used
+#
+# Default: true
+use_indent_func_call_param      = true     # true/false
+
+# The value of the indentation for a continuation line is calculated
+# differently if the statement is:
+# - a declaration: your case with QString fileName ...
+# - an assignment: your case with pSettings = new QSettings( ...
+#
+# At the second case the indentation value might be used twice:
+# - at the assignment
+# - at the function call (if present)
+#
+# To prevent the double use of the indentation value, use this option with the
+# value 'true'.
+#
+# true:  indent_continue will be used only once
+# false: indent_continue will be used every time (default)
+use_indent_continue_only_once   = false    # true/false
+
+# The value might be used twice:
+# - at the assignment
+# - at the opening brace
+#
+# To prevent the double use of the indentation value, use this option with the
+# value 'true'.
+#
+# true:  indentation will be used only once
+# false: indentation will be used every time (default)
+indent_cpp_lambda_only_once     = false    # true/false
+
+# Whether sp_after_angle takes precedence over sp_inside_fparen. This was the
+# historic behavior, but is probably not the desired behavior, so this is off
+# by default.
+use_sp_after_angle_always       = false    # true/false
+
+# Whether to apply special formatting for Qt SIGNAL/SLOT macros. Essentially,
+# this tries to format these so that they match Qt's normalized form (i.e. the
+# result of QMetaObject::normalizedSignature), which can slightly improve the
+# performance of the QObject::connect call, rather than how they would
+# otherwise be formatted.
+#
+# See options_for_QT.cpp for details.
+#
+# Default: true
+use_options_overriding_for_qt_macros = true     # true/false
+
+# If true: the form feed character is removed from the list
+# of whitespace characters.
+# See https://en.cppreference.com/w/cpp/string/byte/isspace
+use_form_feed_no_more_as_whitespace_character = false    # true/false
+
+#
+# Warn levels - 1: error, 2: warning (default), 3: note
+#
+
+# (C#) Warning is given if doing tab-to-\t replacement and we have found one
+# in a C# verbatim string literal.
+#
+# Default: 2
+warn_level_tabs_found_in_verbatim_string_literals = 2        # unsigned number
+
+# Limit the number of loops.
+# Used by uncrustify.cpp to exit from infinite loop.
+# 0: no limit.
+debug_max_number_of_loops       = 0        # number
+
+# Set the number of the line to protocol;
+# Used in the function prot_the_line if the 2. parameter is zero.
+# 0: nothing protocol.
+debug_line_number_to_protocol   = 0        # number
+
+# Meaning of the settings:
+#   Ignore - do not do any changes
+#   Add    - makes sure there is 1 or more space/brace/newline/etc
+#   Force  - makes sure there is exactly 1 space/brace/newline/etc,
+#            behaves like Add in some contexts
+#   Remove - removes space/brace/newline/etc
+#
+#
+# - Token(s) can be treated as specific type(s) with the 'set' option:
+#     `set tokenType tokenString [tokenString...]`
+#
+#     Example:
+#       `set BOOL __AND__ __OR__`
+#
+#     tokenTypes are defined in src/token_enum.h, use them without the
+#     'CT_' prefix: 'CT_BOOL' => 'BOOL'
+#
+#
+# - Token(s) can be treated as type(s) with the 'type' option.
+#     `type tokenString [tokenString...]`
+#
+#     Example:
+#       `type int c_uint_8 Rectangle`
+#
+#     This can also be achieved with `set TYPE int c_uint_8 Rectangle`
+#
+#
+# To embed whitespace in tokenStrings use the '\' escape character, or quote
+# the tokenStrings. These quotes are supported: "'`
+#
+#
+# - Support for the auto detection of languages through the file ending can be
+#   added using the 'file_ext' command.
+#     `file_ext langType langString [langString..]`
+#
+#     Example:
+#       `file_ext CPP .ch .cxx .cpp.in`
+#
+#     langTypes are defined in uncrusify_types.h in the lang_flag_e enum, use
+#     them without the 'LANG_' prefix: 'LANG_CPP' => 'CPP'
+#
+#
+# - Custom macro-based indentation can be set up using 'macro-open',
+#   'macro-else' and 'macro-close'.
+#     `(macro-open | macro-else | macro-close) tokenString`
+#
+#     Example:
+#       `macro-open  BEGIN_TEMPLATE_MESSAGE_MAP`
+#       `macro-open  BEGIN_MESSAGE_MAP`
+#       `macro-close END_MESSAGE_MAP`
+#
+#
+# option(s) with 'not default' value: 67
+#
+
+# Custom types for MicroPython
+type uint qstr
diff --git a/tools/verifygitlog.py b/tools/verifygitlog.py
new file mode 100755
index 000000000..46fec1e0c
--- /dev/null
+++ b/tools/verifygitlog.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+
+# This is an exact duplicate of verifygitlog.py from the main repo.
+
+import re
+import subprocess
+import sys
+
+verbosity = 0  # Show what's going on, 0 1 or 2.
+suggestions = 1  # Set to 0 to not include lengthy suggestions in error messages.
+
+ignore_prefixes = []
+
+
+def verbose(*args):
+    if verbosity:
+        print(*args)
+
+
+def very_verbose(*args):
+    if verbosity > 1:
+        print(*args)
+
+
+class ErrorCollection:
+    # Track errors and warnings as the program runs
+    def __init__(self):
+        self.has_errors = False
+        self.has_warnings = False
+        self.prefix = ""
+
+    def error(self, text):
+        print("error: {}{}".format(self.prefix, text))
+        self.has_errors = True
+
+    def warning(self, text):
+        print("warning: {}{}".format(self.prefix, text))
+        self.has_warnings = True
+
+
+def git_log(pretty_format, *args):
+    # Delete pretty argument from user args so it doesn't interfere with what we do.
+    args = ["git", "log"] + [arg for arg in args if "--pretty" not in args]
+    args.append("--pretty=format:" + pretty_format)
+    very_verbose("git_log", *args)
+    # Generator yielding each output line.
+    for line in subprocess.Popen(args, stdout=subprocess.PIPE).stdout:
+        yield line.decode().rstrip("\r\n")
+
+
+def diagnose_subject_line(subject_line, subject_line_format, err):
+    err.error('Subject line: "' + subject_line + '"')
+    if not subject_line.endswith("."):
+        err.error('* must end with "."')
+    if not re.match(r"^[^!]+: ", subject_line):
+        err.error('* must start with "path: "')
+    if re.match(r"^[^!]+: *$", subject_line):
+        err.error("* must contain a subject after the path.")
+    m = re.match(r"^[^!]+: ([a-z][^ ]*)", subject_line)
+    if m:
+        err.error('* first word of subject ("{}") must be capitalised.'.format(m.group(1)))
+    if re.match(r"^[^!]+: [^ ]+$", subject_line):
+        err.error("* subject must contain more than one word.")
+    err.error("* must match: " + repr(subject_line_format))
+    err.error('* Example: "py/runtime: Add support for foo to bar."')
+
+
+def verify(sha, err):
+    verbose("verify", sha)
+    err.prefix = "commit " + sha + ": "
+
+    # Author and committer email.
+    for line in git_log("%ae%n%ce", sha, "-n1"):
+        very_verbose("email", line)
+        if "noreply" in line:
+            err.error("Unwanted email address: " + line)
+
+    # Message body.
+    raw_body = list(git_log("%B", sha, "-n1"))
+    verify_message_body(raw_body, err)
+
+
+def verify_message_body(raw_body, err):
+    if not raw_body:
+        err.error("Message is empty")
+        return
+
+    # Subject line.
+    subject_line = raw_body[0]
+    for prefix in ignore_prefixes:
+        if subject_line.startswith(prefix):
+            verbose("Skipping ignored commit message")
+            return
+    very_verbose("subject_line", subject_line)
+    subject_line_format = r"^[^!]+: [A-Z]+.+ .+\.$"
+    if not re.match(subject_line_format, subject_line):
+        diagnose_subject_line(subject_line, subject_line_format, err)
+    if len(subject_line) >= 73:
+        err.error("Subject line must be 72 or fewer characters: " + subject_line)
+
+    # Do additional checks on the prefix of the subject line.
+    verify_subject_line_prefix(subject_line.split(": ")[0], err)
+
+    # Second one divides subject and body.
+    if len(raw_body) > 1 and raw_body[1]:
+        err.error("Second message line must be empty: " + raw_body[1])
+
+    # Message body lines.
+    for line in raw_body[2:]:
+        # Long lines with URLs or human names are exempt from the line length rule.
+        if len(line) >= 76 and not (
+            "://" in line
+            or line.startswith("Co-authored-by: ")
+            or line.startswith("Signed-off-by: ")
+        ):
+            err.error("Message lines should be 75 or less characters: " + line)
+
+    if not raw_body[-1].startswith("Signed-off-by: ") or "@" not in raw_body[-1]:
+        err.error('Message must be signed-off. Use "git commit -s".')
+
+
+def verify_subject_line_prefix(prefix, err):
+    ext = (".c", ".h", ".cpp", ".js", ".rst", ".md")
+
+    if prefix.startswith((".", "/")):
+        err.error('Subject prefix cannot begin with "." or "/".')
+
+    if prefix.endswith("/"):
+        err.error('Subject prefix cannot end with "/".')
+
+    if prefix.startswith("ports/"):
+        err.error(
+            'Subject prefix cannot begin with "ports/", start with the name of the port instead.'
+        )
+
+    if prefix.endswith(ext):
+        err.error(
+            "Subject prefix cannot end with a file extension, use the main part of the filename without the extension."
+        )
+
+
+def run(args):
+    verbose("run", *args)
+
+    err = ErrorCollection()
+
+    if "--check-file" in args:
+        filename = args[-1]
+        verbose("checking commit message from", filename)
+        with open(args[-1]) as f:
+            # Remove comment lines as well as any empty lines at the end.
+            lines = [line.rstrip("\r\n") for line in f if not line.startswith("#")]
+            while not lines[-1]:
+                lines.pop()
+            verify_message_body(lines, err)
+    else:  # Normal operation, pass arguments to git log
+        for sha in git_log("%h", *args):
+            verify(sha, err)
+
+    if err.has_errors or err.has_warnings:
+        if suggestions:
+            print("See https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md")
+    else:
+        print("ok")
+    if err.has_errors:
+        sys.exit(1)
+
+
+def show_help():
+    print("usage: verifygitlog.py [-v -n -h --check-file] ...")
+    print("-v  : increase verbosity, can be specified multiple times")
+    print("-n  : do not print multi-line suggestions")
+    print("-h  : print this help message and exit")
+    print(
+        "--check-file : Pass a single argument which is a file containing a candidate commit message"
+    )
+    print(
+        "--ignore-rebase : Skip checking commits with git rebase autosquash prefixes or WIP as a prefix"
+    )
+    print("... : arguments passed to git log to retrieve commits to verify")
+    print("      see https://www.git-scm.com/docs/git-log")
+    print("      passing no arguments at all will verify all commits")
+    print("examples:")
+    print("verifygitlog.py -n10  # Check last 10 commits")
+    print("verifygitlog.py -v master..HEAD  # Check commits since master")
+
+
+if __name__ == "__main__":
+    args = sys.argv[1:]
+    verbosity = args.count("-v")
+    suggestions = args.count("-n") == 0
+    if "--ignore-rebase" in args:
+        args.remove("--ignore-rebase")
+        ignore_prefixes = ["squash!", "fixup!", "amend!", "WIP"]
+
+    if "-h" in args:
+        show_help()
+    else:
+        args = [arg for arg in args if arg not in ["-v", "-n", "-h"]]
+        run(args)
diff --git a/trace/metadata.txt b/trace/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/trace/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/trace/setup.py b/trace/setup.py
deleted file mode 100644
index 9b342adac..000000000
--- a/trace/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-trace',
-      version='0.0.0',
-      description='Dummy trace module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['trace'])
diff --git a/trace/trace.py b/trace/trace.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/traceback/metadata.txt b/traceback/metadata.txt
deleted file mode 100644
index 6b5dc7328..000000000
--- a/traceback/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=micropython-lib
-type=module
-version=0.1
diff --git a/traceback/setup.py b/traceback/setup.py
deleted file mode 100644
index c48853c1e..000000000
--- a/traceback/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-traceback',
-      version='0.1',
-      description='traceback module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['traceback'])
diff --git a/traceback/traceback.py b/traceback/traceback.py
deleted file mode 100644
index a32b0c441..000000000
--- a/traceback/traceback.py
+++ /dev/null
@@ -1,5 +0,0 @@
-def format_tb(tb, limit):
-    return ["traceback.format_tb() not implemented\n"]
-
-def format_exception_only(type, value):
-    return [repr(value) + "\n"]
diff --git a/tty/metadata.txt b/tty/metadata.txt
deleted file mode 100644
index cc1fdba58..000000000
--- a/tty/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 1.0
diff --git a/tty/setup.py b/tty/setup.py
deleted file mode 100644
index cd9f23a79..000000000
--- a/tty/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-tty',
-      version='1.0',
-      description='tty module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['tty'])
diff --git a/types/setup.py b/types/setup.py
deleted file mode 100644
index 41ad422ac..000000000
--- a/types/setup.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from distutils.core import setup
-
-setup(name='micropython-types',
-      version='0.0.1',
-      description='CPython types module ported to MicroPython',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['types'])
diff --git a/uasyncio.core/metadata.txt b/uasyncio.core/metadata.txt
deleted file mode 100644
index 624350ebc..000000000
--- a/uasyncio.core/metadata.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-srctype = micropython-lib
-type = package
-version = 0.9
-author = Paul Sokolovsky
-long_desc = Lightweight implementation of asyncio-like library built around native Python coroutines. (Core event loop).
-depends = logging
diff --git a/uasyncio.core/setup.py b/uasyncio.core/setup.py
deleted file mode 100644
index 898614037..000000000
--- a/uasyncio.core/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-uasyncio.core',
-      version='0.9',
-      description='uasyncio.core module for MicroPython',
-      long_description='Lightweight implementation of asyncio-like library built around native Python coroutines. (Core event loop).',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['uasyncio'],
-      install_requires=['micropython-logging'])
diff --git a/uasyncio.core/uasyncio/core.py b/uasyncio.core/uasyncio/core.py
deleted file mode 100644
index 6867a6690..000000000
--- a/uasyncio.core/uasyncio/core.py
+++ /dev/null
@@ -1,179 +0,0 @@
-try:
-    import utime as time
-except ImportError:
-    import time
-import uheapq as heapq
-import logging
-
-
-log = logging.getLogger("asyncio")
-
-type_gen = type((lambda: (yield))())
-
-class EventLoop:
-
-    def __init__(self):
-        self.q = []
-        self.cnt = 0
-
-    def time(self):
-        return time.time()
-
-    def create_task(self, coro):
-        # CPython 3.4.2
-        self.call_at(0, coro)
-        # CPython asyncio incompatibility: we don't return Task object
-
-    def call_soon(self, callback, *args):
-        self.call_at(0, callback, *args)
-
-    def call_later(self, delay, callback, *args):
-        self.call_at(self.time() + delay, callback, *args)
-
-    def call_at(self, time, callback, *args):
-        # Including self.cnt is a workaround per heapq docs
-        if __debug__:
-            log.debug("Scheduling %s", (time, self.cnt, callback, args))
-        heapq.heappush(self.q, (time, self.cnt, callback, args))
-#        print(self.q)
-        self.cnt += 1
-
-    def wait(self, delay):
-        # Default wait implementation, to be overriden in subclasses
-        # with IO scheduling
-        log.debug("Sleeping for: %s", delay)
-        time.sleep(delay)
-
-    def run_forever(self):
-        while True:
-            if self.q:
-                t, cnt, cb, args = heapq.heappop(self.q)
-                if __debug__:
-                    log.debug("Next coroutine to run: %s", (t, cnt, cb, args))
-#                __main__.mem_info()
-                tnow = self.time()
-                delay = t - tnow
-                if delay > 0:
-                    self.wait(delay)
-            else:
-                self.wait(-1)
-                # Assuming IO completion scheduled some tasks
-                continue
-            if callable(cb):
-                cb(*args)
-            else:
-                delay = 0
-                try:
-                    if args == ():
-                        args = (None,)
-                    if __debug__:
-                        log.debug("Coroutine %s send args: %s", cb, args)
-                    ret = cb.send(*args)
-                    if __debug__:
-                        log.debug("Coroutine %s yield result: %s", cb, ret)
-                    if isinstance(ret, SysCall1):
-                        arg = ret.arg
-                        if isinstance(ret, Sleep):
-                            delay = arg
-                        elif isinstance(ret, IORead):
-#                            self.add_reader(ret.obj.fileno(), lambda self, c, f: self.call_soon(c, f), self, cb, ret.obj)
-#                            self.add_reader(ret.obj.fileno(), lambda c, f: self.call_soon(c, f), cb, ret.obj)
-#                            self.add_reader(arg.fileno(), lambda cb: self.call_soon(cb), cb)
-                            self.add_reader(arg.fileno(), cb)
-                            continue
-                        elif isinstance(ret, IOWrite):
-#                            self.add_writer(arg.fileno(), lambda cb: self.call_soon(cb), cb)
-                            self.add_writer(arg.fileno(), cb)
-                            continue
-                        elif isinstance(ret, IOReadDone):
-                            self.remove_reader(arg.fileno())
-                        elif isinstance(ret, IOWriteDone):
-                            self.remove_writer(arg.fileno())
-                        elif isinstance(ret, StopLoop):
-                            return arg
-                    elif isinstance(ret, type_gen):
-                        self.call_soon(ret)
-                    elif ret is None:
-                        # Just reschedule
-                        pass
-                    else:
-                        assert False, "Unsupported coroutine yield value: %r (of type %r)" % (ret, type(ret))
-                except StopIteration as e:
-                    if __debug__:
-                        log.debug("Coroutine finished: %s", cb)
-                    continue
-                self.call_later(delay, cb, *args)
-
-    def run_until_complete(self, coro):
-        def _run_and_stop():
-            yield from coro
-            yield StopLoop(0)
-        self.call_soon(_run_and_stop())
-        self.run_forever()
-
-    def close(self):
-        pass
-
-
-class SysCall:
-
-    def __init__(self, *args):
-        self.args = args
-
-    def handle(self):
-        raise NotImplementedError
-
-# Optimized syscall with 1 arg
-class SysCall1(SysCall):
-
-    def __init__(self, arg):
-        self.arg = arg
-
-class Sleep(SysCall1):
-    pass
-
-class StopLoop(SysCall1):
-    pass
-
-class IORead(SysCall1):
-    pass
-
-class IOWrite(SysCall1):
-    pass
-
-class IOReadDone(SysCall1):
-    pass
-
-class IOWriteDone(SysCall1):
-    pass
-
-
-_event_loop = None
-_event_loop_class = EventLoop
-def get_event_loop():
-    global _event_loop
-    if _event_loop is None:
-        _event_loop = _event_loop_class()
-    return _event_loop
-
-def sleep(secs):
-    yield Sleep(secs)
-
-def coroutine(f):
-    return f
-
-#
-# The functions below are deprecated in uasyncio, and provided only
-# for compatibility with CPython asyncio
-#
-
-def async(coro, loop=_event_loop):
-    _event_loop.call_soon(coro)
-    # CPython asyncio incompatibility: we don't return Task object
-    return coro
-
-
-# CPython asyncio incompatibility: Task is a function, not a class (for efficiency)
-def Task(coro, loop=_event_loop):
-    # Same as async()
-    _event_loop.call_soon(coro)
diff --git a/uasyncio.queues/metadata.txt b/uasyncio.queues/metadata.txt
deleted file mode 100644
index 9e4c1f191..000000000
--- a/uasyncio.queues/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = package
-version = 0.1
-long_desc = Port of asyncio.queues to uasyncio.
-depends = uasyncio.core, collections.deque
diff --git a/uasyncio.queues/setup.py b/uasyncio.queues/setup.py
deleted file mode 100644
index 22b0660a1..000000000
--- a/uasyncio.queues/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-uasyncio.queues',
-      version='0.1',
-      description='uasyncio.queues module for MicroPython',
-      long_description='Port of asyncio.queues to uasyncio.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['uasyncio'],
-      install_requires=['micropython-uasyncio.core', 'micropython-collections.deque'])
diff --git a/uasyncio.queues/tests/test.py b/uasyncio.queues/tests/test.py
deleted file mode 100644
index 72b3d85c3..000000000
--- a/uasyncio.queues/tests/test.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from unittest import TestCase, run_class
-import sys
-sys.path.insert(0, '../uasyncio')
-import queues
-
-
-class QueueTestCase(TestCase):
-
-    def _val(self, gen):
-        """Returns val from generator."""
-        while True:
-            try:
-                gen.send(None)
-            except StopIteration as e:
-                return e.value
-
-    def test_get_put(self):
-        q = queues.Queue(maxsize=1)
-        self._val(q.put(42))
-        self.assertEqual(self._val(q.get()), 42)
-
-    def test_get_put_nowait(self):
-        q = queues.Queue(maxsize=1)
-        q.put_nowait(12)
-        try:
-            q.put_nowait(42)
-            self.assertTrue(False)
-        except Exception as e:
-            self.assertEqual(type(e), queues.QueueFull)
-        self.assertEqual(q.get_nowait(), 12)
-        try:
-            q.get_nowait()
-            self.assertTrue(False)
-        except Exception as e:
-            self.assertEqual(type(e), queues.QueueEmpty)
-
-    def test_qsize(self):
-        q = queues.Queue()
-        for n in range(10):
-            q.put_nowait(10)
-        self.assertEqual(q.qsize(), 10)
-
-    def test_empty(self):
-        q = queues.Queue()
-        self.assertTrue(q.empty())
-        q.put_nowait(10)
-        self.assertFalse(q.empty())
-
-    def test_full(self):
-        q = queues.Queue(maxsize=1)
-        self.assertFalse(q.full())
-        q.put_nowait(10)
-        self.assertTrue(q.full())
-
-
-if __name__ == '__main__':
-    run_class(QueueTestCase)
diff --git a/uasyncio.queues/uasyncio/queues.py b/uasyncio.queues/uasyncio/queues.py
deleted file mode 100644
index 4a8ae5fe5..000000000
--- a/uasyncio.queues/uasyncio/queues.py
+++ /dev/null
@@ -1,94 +0,0 @@
-from collections.deque import deque
-from uasyncio.core import sleep
-
-
-class QueueEmpty(Exception):
-    """Exception raised by get_nowait()."""
-
-
-class QueueFull(Exception):
-    """Exception raised by put_nowait()."""
-
-
-class Queue:
-    """A queue, useful for coordinating producer and consumer coroutines.
-
-    If maxsize is less than or equal to zero, the queue size is infinite. If it
-    is an integer greater than 0, then "yield from put()" will block when the
-    queue reaches maxsize, until an item is removed by get().
-
-    Unlike the standard library Queue, you can reliably know this Queue's size
-    with qsize(), since your single-threaded uasyncio application won't be
-    interrupted between calling qsize() and doing an operation on the Queue.
-    """
-    _attempt_delay = 0.1
-
-    def __init__(self, maxsize=0):
-        self.maxsize = maxsize
-        self._queue = deque()
-
-    def _get(self):
-        return self._queue.popleft()
-
-    def get(self):
-        """Returns generator, which can be used for getting (and removing)
-        an item from a queue.
-
-        Usage::
-
-            item = yield from queue.get()
-        """
-        while not self._queue:
-            yield from sleep(self._attempt_delay)
-        return self._get()
-
-    def get_nowait(self):
-        """Remove and return an item from the queue.
-
-        Return an item if one is immediately available, else raise QueueEmpty.
-        """
-        if not self._queue:
-            raise QueueEmpty()
-        return self._get()
-
-    def _put(self, val):
-        self._queue.append(val)
-
-    def put(self, val):
-        """Returns generator which can be used for putting item in a queue.
-
-        Usage::
-
-            yield from queue.put(item)
-        """
-        while self.qsize() > self.maxsize and self.maxsize:
-            yield from sleep(self._attempt_delay)
-        self._put(val)
-
-    def put_nowait(self, val):
-        """Put an item into the queue without blocking.
-
-        If no free slot is immediately available, raise QueueFull.
-        """
-        if self.qsize() >= self.maxsize and self.maxsize:
-            raise QueueFull()
-        self._put(val)
-
-    def qsize(self):
-        """Number of items in the queue."""
-        return len(self._queue)
-
-    def empty(self):
-        """Return True if the queue is empty, False otherwise."""
-        return not self._queue
-
-    def full(self):
-        """Return True if there are maxsize items in the queue.
-
-        Note: if the Queue was initialized with maxsize=0 (the default),
-        then full() is never True.
-        """
-        if self.maxsize <= 0:
-            return False
-        else:
-            return self.qsize() >= self.maxsize
diff --git a/uasyncio/README.test b/uasyncio/README.test
deleted file mode 100644
index 89cd7ecd6..000000000
--- a/uasyncio/README.test
+++ /dev/null
@@ -1,45 +0,0 @@
-Testing and Validating
-----------------------
-
-To test uasyncio correctness and performance, HTTP server samples can be
-used. The simplest test is with test_http_server.py and Apache Benchmark
-(ab). In one window, run:
-
-micropython -O test_http_server.py
-
-(-O is needed to short-circuit debug logging calls.)
-
-In another:
-
-ab -n10000 -c10 http://localhost:8081/
-
-ab tests that all responses have the same length, but doesn't check
-content. test_http_server.py also serves very short, static reply.
-
-
-For more heavy testing, test_http_server_heavy.py is provided. It serves
-large response split among several async writes. It is also dynamic -
-includes incrementing counter, so each response will be different. The
-response size generates is more 4Mb, because under Linux, socket writes
-can buffer up to 4Mb of content (this appear to be controlled by
-/proc/sys/net/ipv4/tcp_wmem and not /proc/sys/net/core/wmem_default).
-test_http_server_heavy.py also includes (trivial) handling of
-client-induced errors like EPIPE and ECONNRESET. To validate content
-served, a post-hook script for "boom" tool
-(https://github.com/tarekziade/boom) is provided.
-
-Before start, you may want to bump .listen() value in uasyncio/__init__.py
-from default 10 to at least 30.
-
-Start:
-
-micropython -X heapsize=300000000 -O test_http_server_heavy.py
-
-(Yes, that's 300Mb of heap - we'll be serving 4+Mb of content with 30
-concurrent connections).
-
-And:
-
-PYTHONPATH=. boom -n1000 -c30 http://localhost:8081 --post-hook=boom_uasyncio.validate
-
-There should be no Python exceptions in the output.
diff --git a/uasyncio/benchmark/boom_uasyncio.py b/uasyncio/benchmark/boom_uasyncio.py
deleted file mode 100644
index def5bfb19..000000000
--- a/uasyncio/benchmark/boom_uasyncio.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# This is validation script for "boom" tool https://github.com/tarekziade/boom
-# To use it:
-#
-# boom -n1000 --post-hook=boom_uasyncio.validate <rest of boom args>
-#
-# Note that if you'll use other -n value, you should update NUM_REQS below
-# to match.
-#
-
-NUM_REQS = 1000
-seen = []
-cnt = 0
-
-def validate(resp):
-    global cnt
-    t = resp.text
-    l = t.split("\r\n", 1)[0]
-    no = int(l.split()[1])
-    seen.append(no)
-    c = t.count(l + "\r\n")
-    assert c == 400101
-    assert t.endswith("=== END ===")
-
-    cnt += 1
-    if cnt == NUM_REQS:
-        seen.sort()
-        print
-        print seen
-        print
-        el = None
-        for i in seen:
-            if el is None:
-                el = i
-            else:
-                el += 1
-                assert i == el
-    return resp
diff --git a/uasyncio/benchmark/test-ab-light.sh b/uasyncio/benchmark/test-ab-light.sh
deleted file mode 100755
index 383f1aa0a..000000000
--- a/uasyncio/benchmark/test-ab-light.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/sh
-#
-# This in one-shot scripts to test "light load" uasyncio HTTP server using
-# Apache Bench (ab).
-#
-
-#python3.4.2 test_http_server_light.py &
-#micropython -O test_http_server_light.py &
-
-#python3.4.2 test_http_server_medium.py &
-micropython -O -X heapsize=200wK test_http_server_medium.py &
-
-sleep 1
-
-ab -n10000 -c100 http://localhost:8081/
-
-kill %1
diff --git a/uasyncio/benchmark/test-boom-heavy.sh b/uasyncio/benchmark/test-boom-heavy.sh
deleted file mode 100755
index a977806a5..000000000
--- a/uasyncio/benchmark/test-boom-heavy.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/sh
-#
-# This in one-shot scripts to test "heavy load" uasyncio HTTP server using
-# Boom tool https://github.com/tarekziade/boom .
-#
-# Note that this script doesn't test performance, but rather test functional
-# correctness of uasyncio server implementation, while serving large amounts
-# of data (guaranteedly more than a socket buffer). Thus, this script should
-# not be used for benchmarking.
-#
-
-if [ ! -d .venv-boom ]; then
-    virtualenv .venv-boom
-    . .venv-boom/bin/activate
-    # PyPI currently has 0.8 which is too old
-    #pip install boom
-    pip install git+https://github.com/tarekziade/boom
-else
-    . .venv-boom/bin/activate
-fi
-
-
-micropython -X heapsize=300000000 -O test_http_server_heavy.py &
-sleep 1
-
-PYTHONPATH=. boom -n1000 -c30 http://localhost:8081 --post-hook=boom_uasyncio.validate
-
-kill %1
diff --git a/uasyncio/benchmark/test_http_server_heavy.py b/uasyncio/benchmark/test_http_server_heavy.py
deleted file mode 100644
index 2a7552db3..000000000
--- a/uasyncio/benchmark/test_http_server_heavy.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import uasyncio as asyncio
-import signal
-import errno
-
-
-cnt = 0
-
-@asyncio.coroutine
-def serve(reader, writer):
-    global cnt
-    #s = "Hello.\r\n"
-    s = "Hello. %07d\r\n" % cnt
-    cnt += 1
-    yield from reader.read()
-    yield from writer.awrite("HTTP/1.0 200 OK\r\n\r\n")
-    try:
-        yield from writer.awrite(s)
-        yield from writer.awrite(s * 100)
-        yield from writer.awrite(s * 400000)
-        yield from writer.awrite("=== END ===")
-        yield from writer.aclose()
-    except OSError as e:
-        if e.args[0] == errno.EPIPE:
-            print("EPIPE")
-        elif e.args[0] == errno.ECONNRESET:
-            print("ECONNRESET")
-        else:
-            raise
-
-
-import logging
-logging.basicConfig(level=logging.INFO)
-#logging.basicConfig(level=logging.DEBUG)
-signal.signal(signal.SIGPIPE, signal.SIG_IGN)
-loop = asyncio.get_event_loop()
-#mem_info()
-loop.call_soon(asyncio.start_server(serve, "0.0.0.0", 8081, backlog=100))
-loop.run_forever()
-loop.close()
diff --git a/uasyncio/benchmark/test_http_server_light.py b/uasyncio/benchmark/test_http_server_light.py
deleted file mode 100644
index 2fbb92a18..000000000
--- a/uasyncio/benchmark/test_http_server_light.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import uasyncio as asyncio
-
-
-@asyncio.coroutine
-def serve(reader, writer):
-    #print(reader, writer)
-    #print("================")
-    yield from reader.read(256)
-    yield from writer.awrite("HTTP/1.0 200 OK\r\n\r\nHello.\r\n")
-    yield from writer.aclose()
-    #print("Finished processing request")
-
-
-import logging
-#logging.basicConfig(level=logging.INFO)
-logging.basicConfig(level=logging.DEBUG)
-loop = asyncio.get_event_loop()
-#mem_info()
-loop.create_task(asyncio.start_server(serve, "127.0.0.1", 8081, backlog=100))
-loop.run_forever()
-loop.close()
diff --git a/uasyncio/benchmark/test_http_server_medium.py b/uasyncio/benchmark/test_http_server_medium.py
deleted file mode 100644
index 7d6f3cbea..000000000
--- a/uasyncio/benchmark/test_http_server_medium.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import uasyncio as asyncio
-
-resp = "HTTP/1.0 200 OK\r\n\r\n" + "Hello.\r\n" * 1500
-
-@asyncio.coroutine
-def serve(reader, writer):
-    #print(reader, writer)
-    #print("================")
-    yield from reader.read(256)
-    yield from writer.awrite(resp)
-    yield from writer.aclose()
-    #print("Finished processing request")
-
-
-import logging
-#logging.basicConfig(level=logging.INFO)
-logging.basicConfig(level=logging.DEBUG)
-loop = asyncio.get_event_loop()
-#mem_info()
-loop.create_task(asyncio.start_server(serve, "127.0.0.1", 8081, backlog=100))
-loop.run_forever()
-loop.close()
diff --git a/uasyncio/metadata.txt b/uasyncio/metadata.txt
deleted file mode 100644
index c61a11408..000000000
--- a/uasyncio/metadata.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-srctype = micropython-lib
-type = package
-version = 0.9
-author = Paul Sokolovsky
-long_desc = Lightweight asyncio-like library built around native Python coroutines, not around un-Python devices like callback mess.
-depends = errno, uasyncio.core
diff --git a/uasyncio/setup.py b/uasyncio/setup.py
deleted file mode 100644
index 395af0f54..000000000
--- a/uasyncio/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-uasyncio',
-      version='0.9',
-      description='uasyncio module for MicroPython',
-      long_description='Lightweight asyncio-like library built around native Python coroutines, not around un-Python devices like callback mess.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['uasyncio'],
-      install_requires=['micropython-errno', 'micropython-uasyncio.core'])
diff --git a/uasyncio/test_call_soon.py b/uasyncio/test_call_soon.py
deleted file mode 100644
index 99ccfefbc..000000000
--- a/uasyncio/test_call_soon.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import uasyncio as asyncio
-import time
-
-
-def cb():
-    print("callback")
-    time.sleep(0.5)
-    loop.call_soon(cb)
-
-
-loop = asyncio.get_event_loop()
-loop.call_soon(cb)
-loop.run_forever()
diff --git a/uasyncio/test_http_client.py b/uasyncio/test_http_client.py
deleted file mode 100644
index 1c6cf6876..000000000
--- a/uasyncio/test_http_client.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import uasyncio as asyncio
-
-@asyncio.coroutine
-def print_http_headers(url):
-    reader, writer = yield from asyncio.open_connection(url, 80)
-    print(reader, writer)
-    print("================")
-    query = "GET / HTTP/1.0\r\n\r\n"
-    yield from writer.awrite(query.encode('latin-1'))
-    while True:
-        line = yield from reader.readline()
-        if not line:
-            break
-        if line:
-            print(line.rstrip())
-
-import logging
-logging.basicConfig(level=logging.INFO)
-url = "google.com"
-loop = asyncio.get_event_loop()
-#task = asyncio.async(print_http_headers(url))
-#loop.run_until_complete(task)
-loop.run_until_complete(print_http_headers(url))
-loop.close()
diff --git a/uasyncio/test_http_server.py b/uasyncio/test_http_server.py
deleted file mode 100644
index 9e2cf894d..000000000
--- a/uasyncio/test_http_server.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import uasyncio as asyncio
-
-@asyncio.coroutine
-def serve(reader, writer):
-    print(reader, writer)
-    print("================")
-    print((yield from reader.read()))
-    yield from writer.awrite("HTTP/1.0 200 OK\r\n\r\nHello.\r\n")
-    print("After response write")
-    yield from writer.aclose()
-    print("Finished processing request")
-
-
-import logging
-#logging.basicConfig(level=logging.INFO)
-logging.basicConfig(level=logging.DEBUG)
-loop = asyncio.get_event_loop()
-mem_info()
-loop.call_soon(asyncio.start_server(serve, "127.0.0.1", 8081))
-loop.run_forever()
-loop.close()
diff --git a/uasyncio/uasyncio/__init__.py b/uasyncio/uasyncio/__init__.py
deleted file mode 100644
index 504f1e928..000000000
--- a/uasyncio/uasyncio/__init__.py
+++ /dev/null
@@ -1,206 +0,0 @@
-import errno
-import uselect as select
-import usocket as _socket
-from uasyncio.core import *
-
-
-class EpollEventLoop(EventLoop):
-
-    def __init__(self):
-        EventLoop.__init__(self)
-        self.poller = select.poll()
-        self.objmap = {}
-
-    def add_reader(self, fd, cb, *args):
-        if __debug__:
-            log.debug("add_reader%s", (fd, cb, args))
-        if args:
-            self.poller.register(fd, select.POLLIN)
-            self.objmap[fd] = (cb, args)
-        else:
-            self.poller.register(fd, select.POLLIN)
-            self.objmap[fd] = cb
-
-    def remove_reader(self, fd):
-        if __debug__:
-            log.debug("remove_reader(%s)", fd)
-        self.poller.unregister(fd)
-        del self.objmap[fd]
-
-    def add_writer(self, fd, cb, *args):
-        if __debug__:
-            log.debug("add_writer%s", (fd, cb, args))
-        if args:
-            self.poller.register(fd, select.POLLOUT)
-            self.objmap[fd] = (cb, args)
-        else:
-            self.poller.register(fd, select.POLLOUT)
-            self.objmap[fd] = cb
-
-    def remove_writer(self, fd):
-        if __debug__:
-            log.debug("remove_writer(%s)", fd)
-        try:
-            self.poller.unregister(fd)
-            self.objmap.pop(fd, None)
-        except OSError as e:
-            # StreamWriter.awrite() first tries to write to an fd,
-            # and if that succeeds, yield IOWrite may never be called
-            # for that fd, and it will never be added to poller. So,
-            # ignore such error.
-            if e.args[0] != errno.ENOENT:
-                raise
-
-    def wait(self, delay):
-        if __debug__:
-            log.debug("epoll.wait(%d)", delay)
-        # We need one-shot behavior (second arg of 1 to .poll())
-        if delay == -1:
-            res = self.poller.poll(-1, 1)
-        else:
-            res = self.poller.poll(int(delay * 1000), 1)
-        #log.debug("epoll result: %s", res)
-        for fd, ev in res:
-            cb = self.objmap[fd]
-            if __debug__:
-                log.debug("Calling IO callback: %r", cb)
-            if isinstance(cb, tuple):
-                cb[0](*cb[1])
-            else:
-                self.call_soon(cb)
-
-
-class StreamReader:
-
-    def __init__(self, s):
-        self.s = s
-
-    def read(self, n=-1):
-        yield IORead(self.s)
-        while True:
-            res = self.s.read(n)
-            if res is not None:
-                break
-            log.warn("Empty read")
-        if not res:
-            yield IOReadDone(self.s)
-        return res
-
-    def readline(self):
-        if __debug__:
-            log.debug("StreamReader.readline()")
-        yield IORead(self.s)
-#        if __debug__:
-#            log.debug("StreamReader.readline(): after IORead: %s", s)
-        while True:
-            res = self.s.readline()
-            if res is not None:
-                break
-            log.warn("Empty read")
-        if not res:
-            yield IOReadDone(self.s)
-        if __debug__:
-            log.debug("StreamReader.readline(): res: %s", res)
-        return res
-
-    def aclose(self):
-        yield IOReadDone(self.s)
-        self.s.close()
-
-    def __repr__(self):
-        return "<StreamReader %r>" % self.s
-
-
-class StreamWriter:
-
-    def __init__(self, s, extra):
-        self.s = s
-        self.extra = extra
-
-    def awrite(self, buf):
-        # This method is called awrite (async write) to not proliferate
-        # incompatibility with original asyncio. Unlike original asyncio
-        # whose .write() method is both not a coroutine and guaranteed
-        # to return immediately (which means it has to buffer all the
-        # data), this method is a coroutine.
-        sz = len(buf)
-        if __debug__:
-            log.debug("StreamWriter.awrite(): spooling %d bytes", sz)
-        while True:
-            res = self.s.write(buf)
-            # If we spooled everything, return immediately
-            if res == sz:
-                if __debug__:
-                    log.debug("StreamWriter.awrite(): completed spooling %d bytes", res)
-                return
-            if res is None:
-                res = 0
-            if __debug__:
-                log.debug("StreamWriter.awrite(): spooled partial %d bytes", res)
-            assert res < sz
-            buf = buf[res:]
-            sz -= res
-            yield IOWrite(self.s)
-            #assert s2.fileno() == self.s.fileno()
-            if __debug__:
-                log.debug("StreamWriter.awrite(): can write more")
-
-    def aclose(self):
-        yield IOWriteDone(self.s)
-        self.s.close()
-
-    def get_extra_info(self, name, default=None):
-        return self.extra.get(name, default)
-
-    def __repr__(self):
-        return "<StreamWriter %r>" % self.s
-
-
-def open_connection(host, port):
-    if __debug__:
-        log.debug("open_connection(%s, %s)", host, port)
-    s = _socket.socket()
-    s.setblocking(False)
-    ai = _socket.getaddrinfo(host, port)
-    addr = ai[0][4]
-    try:
-        s.connect(addr)
-    except OSError as e:
-        if e.args[0] != errno.EINPROGRESS:
-            raise
-    if __debug__:
-        log.debug("open_connection: After connect")
-    yield IOWrite(s)
-#    if __debug__:
-#        assert s2.fileno() == s.fileno()
-    if __debug__:
-        log.debug("open_connection: After iowait: %s", s)
-    return StreamReader(s), StreamWriter(s, {})
-
-
-def start_server(client_coro, host, port, backlog=10):
-    log.debug("start_server(%s, %s)", host, port)
-    s = _socket.socket()
-    s.setblocking(False)
-
-    ai = _socket.getaddrinfo(host, port)
-    addr = ai[0][4]
-    s.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
-    s.bind(addr)
-    s.listen(backlog)
-    while True:
-        if __debug__:
-            log.debug("start_server: Before accept")
-        yield IORead(s)
-        if __debug__:
-            log.debug("start_server: After iowait")
-        s2, client_addr = s.accept()
-        s2.setblocking(False)
-        if __debug__:
-            log.debug("start_server: After accept: %s", s2)
-        extra = {"peername": client_addr}
-        yield client_coro(StreamReader(s2), StreamWriter(s2, extra))
-
-
-import uasyncio.core
-uasyncio.core._event_loop_class = EpollEventLoop
diff --git a/ucontextlib/metadata.txt b/ucontextlib/metadata.txt
deleted file mode 100644
index 1f515fd52..000000000
--- a/ucontextlib/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1
-license = Python
-long_desc = Minimal subset of contextlib for MicroPython low-memory ports
diff --git a/ucontextlib/setup.py b/ucontextlib/setup.py
deleted file mode 100644
index 3568fb1b8..000000000
--- a/ucontextlib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-ucontextlib',
-      version='0.1',
-      description='ucontextlib module for MicroPython',
-      long_description='Minimal subset of contextlib for MicroPython low-memory ports',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['ucontextlib'])
diff --git a/ucontextlib/tests.py b/ucontextlib/tests.py
deleted file mode 100644
index c5d29ecce..000000000
--- a/ucontextlib/tests.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import unittest
-from ucontextlib import contextmanager
-
-
-class ContextManagerTestCase(unittest.TestCase):
-
-    def setUp(self):
-        self._history = []
-
-        @contextmanager
-        def manager(x):
-            self._history.append('start')
-            try:
-                yield x
-            finally:
-                self._history.append('finish')
-
-        self._manager = manager
-
-    def test_context_manager(self):
-        with self._manager(123) as x:
-            self.assertEqual(x, 123)
-        self.assertEqual(self._history, ['start', 'finish'])
-
-    def test_context_manager_on_error(self):
-        exc = Exception()
-        try:
-            with self._manager(123) as x:
-                raise exc
-        except Exception as e:
-            self.assertEqual(exc, e)
-        self.assertEqual(self._history, ['start', 'finish'])
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/ucurses/metadata.txt b/ucurses/metadata.txt
deleted file mode 100644
index 4643866d1..000000000
--- a/ucurses/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = package
-version = 0.1.1
-author = Paul Sokolovsky
-depends = os, tty, select
diff --git a/ucurses/setup.py b/ucurses/setup.py
deleted file mode 100644
index 6eaf6457b..000000000
--- a/ucurses/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-ucurses',
-      version='0.1.1',
-      description='ucurses module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['ucurses'],
-      install_requires=['micropython-os', 'micropython-tty', 'micropython-select'])
diff --git a/ucurses/ucurses/__init__.py b/ucurses/ucurses/__init__.py
deleted file mode 100644
index 98cb9230b..000000000
--- a/ucurses/ucurses/__init__.py
+++ /dev/null
@@ -1,326 +0,0 @@
-import os
-import tty, termios
-import select
-
-COLOR_BLACK    = 0
-COLOR_RED      = 1
-COLOR_GREEN    = 2
-COLOR_YELLOW   = 3
-COLOR_BLUE     = 4
-COLOR_MAGENTA  = 5
-COLOR_CYAN     = 6
-COLOR_WHITE    = 7
-
-A_NORMAL = 0
-A_BOLD = 1
-A_UNDERLINE = 2
-A_REVERSE = 4
-A_STANDOUT = A_REVERSE
-
-ATTRMAP = {
-A_NORMAL: b"\x1b[0m",
-A_BOLD: b"\x1b[1m",  # Some terminal emulators don't render bold by default, then use 4m for underline
-A_REVERSE: b"\x1b[7m",
-}
-
-# Use http://www.utf8-chartable.de/unicode-utf8-table.pl
-# for utf-8 pseudographic reference
-# "─"
-ACS_HLINE = b"\xe2\x94\x80"
-# "│"
-ACS_VLINE = b"\xe2\x94\x82"
-# "┌"
-ACS_ULCORNER = b"\xe2\x94\x8c"
-# "┐"
-ACS_URCORNER = b"\xe2\x94\x90"
-# "└"
-ACS_LLCORNER = b"\xe2\x94\x94"
-# "┘"
-ACS_LRCORNER = b"\xe2\x94\x98"
-
-KEY_F1 = 1031
-KEY_RESIZE = 1100
-KEY_MOUSE = 1101
-KEY_BTAB = 1090
-
-KEY_UP = 1001
-KEY_DOWN = 1002
-KEY_LEFT = 1003
-KEY_RIGHT = 1004
-KEY_HOME = 1005
-KEY_END = 1006
-KEY_PGUP = 1007
-KEY_PGDN = 1008
-KEY_QUIT = 1009
-KEY_ENTER = 1010
-KEY_BACKSPACE = 1011
-KEY_DELETE = 1012
-KEY_ESC = 0x1b
-
-KEY_DC = KEY_DELETE
-KEY_PPAGE = KEY_PGUP
-KEY_NPAGE = KEY_PGDN
-
-KEYMAP = {
-b"\x1b[A": KEY_UP,
-b"\x1b[B": KEY_DOWN,
-b"\x1b[D": KEY_LEFT,
-b"\x1b[C": KEY_RIGHT,
-b"\x1bOH": KEY_HOME,
-b"\x1bOF": KEY_END,
-b"\x1b[1~": KEY_HOME,
-b"\x1b[4~": KEY_END,
-b"\x1b[5~": KEY_PGUP,
-b"\x1b[6~": KEY_PGDN,
-b"\x03": KEY_QUIT,
-b"\r": KEY_ENTER,
-b"\x7f": KEY_BACKSPACE,
-b"\x1b[3~": KEY_DELETE,
-
-b"\x1bOA": KEY_UP,
-b"\x1bOB": KEY_DOWN,
-b"\x1bOD": KEY_LEFT,
-b"\x1bOC": KEY_RIGHT,
-b"\x1bOP": KEY_F1,
-b"\x1b": KEY_ESC,
-
-b"\x1b[Z": KEY_BTAB,
-}
-
-ALL_MOUSE_EVENTS = 0xff
-
-
-def _wr(s):
-    # TODO: When Python is 3.5, update this to use only bytes
-    if isinstance(s, str):
-        s = bytes(s, "utf-8")
-    os.write(1, s)
-
-def _move(row, col):
-    # TODO: When Python is 3.5, update this to use bytes
-    _wr("\x1b[%d;%dH" % (row + 1, col + 1))
-
-# Clear specified number of positions
-def _clear_num_pos(num):
-    if num > 0:
-        _wr("\x1b[%dX" % num)
-
-def _draw_box(left, top, width, height):
-    bottom = top + height - 1
-    _move(top, left)
-    _wr(ACS_ULCORNER)
-    hor = ACS_HLINE * (width - 2)
-    _wr(hor)
-    _wr(ACS_URCORNER)
-
-    _move(bottom, left)
-    _wr(ACS_LLCORNER)
-    _wr(hor)
-    _wr(ACS_LRCORNER)
-
-    top += 1
-    while top < bottom:
-        _move(top, left)
-        _wr(ACS_VLINE)
-        _move(top, left + width - 1)
-        _wr(ACS_VLINE)
-        top += 1
-
-
-class error(Exception):
-    pass
-
-
-class Window:
-
-    def __init__(self, lines, cols, y, x):
-        self.lines = lines
-        self.cols = cols
-        self.y = y
-        self.x = x
-        self.bkgattr = A_NORMAL
-        self.keybuf = None
-        self.keyi = 0
-        self.keydelay = -1
-
-    def _goto(self, row, col):
-        _move(self.y + row, self.x + col)
-
-    def move(self, y, x):
-        # Maybe need to cache coords?
-        self._goto(y, x)
-
-    def getmaxyx(self):
-        return (self.lines, self.cols)
-
-    def addstr(self, y, x, str, attr=A_NORMAL):
-        self._goto(y, x)
-        # TODO: Should be "ORed"
-        if attr == A_NORMAL:
-            attr = self.bkgattr
-        if attr != A_NORMAL:
-            _wr(ATTRMAP[attr])
-            _wr(str)
-            _wr(ATTRMAP[A_NORMAL])
-        else:
-            _wr(str)
-
-    def addnstr(self, y, x, str, n, attr=A_NORMAL):
-        self.addstr(y, x, str[:n], attr)
-
-    def addch(self, y, x, ch, attr=A_NORMAL):
-        if isinstance(ch, int):
-            ch = chr(ch)
-        self.addstr(y, x, ch, attr)
-
-    def attron(self, attr):
-        pass
-
-    def attroff(self, attr):
-        pass
-
-    def attrset(self, attr):
-        pass
-
-    def bkgdset(self, ch, attr=A_NORMAL):
-        self.bkgattr = attr
-
-    def erase(self):
-        for i in range(self.lines):
-            self._goto(i, 0)
-            _clear_num_pos(self.cols)
-
-    def border(self):
-        _draw_box(self.x, self.y, self.cols, self.lines)
-
-    def hline(self, y, x, ch, n):
-        self.move(y, x)
-        _wr(ch * n)
-
-    def vline(self, y, x, ch, n):
-        for i in range(n):
-            self.move(y + i, x)
-            _wr(ch)
-
-    def refresh(self):
-        pass
-
-    def redrawwin(self):
-        pass
-
-    def keypad(self, yes):
-        pass
-
-    def timeout(self, delay):
-        self.keydelay = delay
-
-    def nodelay(self, yes):
-        if yes:
-            self.keydelay = 0
-        else:
-            self.keydelay = -1
-
-    def getch(self):
-        if self.keybuf and self.keyi < len(self.keybuf):
-            c = self.keybuf[self.keyi]
-            self.keyi += 1
-            return c
-
-        if self.keydelay >= 0:
-            USE_EPOLL = 1
-            if USE_EPOLL:
-                poll = select.epoll()
-                poll.register(0, select.EPOLLIN)
-                res = poll.poll(self.keydelay / 1000)
-                poll.unregister(0)
-                poll.close()
-            else:
-                res = select.select([0], [], [], self.keydelay / 1000)[0]
-            if not res:
-                return -1
-
-        key = os.read(0, 32)
-        if key[0] != 0x1b:
-            self.keybuf = key
-            self.keyi = 1
-            key = key[0]
-
-        else:
-            if key in KEYMAP:
-                key = KEYMAP[key]
-            else:
-                assert False, repr(key)
-        return key
-
-
-SCREEN = Window(24, 80, 0, 0)
-org_termios = None
-
-
-def wrapper(func):
-    global org_termios
-    org_termios = termios.tcgetattr(0)
-    res = func()
-    endwin()
-    return res
-
-def initscr():
-    global org_termios
-    org_termios = termios.tcgetattr(0)
-    return SCREEN
-
-def doupdate():
-    pass
-
-def endwin():
-    global org_termios
-    _wr(b"\r")
-    termios.tcsetattr(0, termios.TCSANOW, org_termios)
-
-def raw():
-    tty.setraw(0)
-
-def cbreak():
-    #TODO
-    pass
-
-def nocbreak():
-    #TODO
-    pass
-
-def echo():
-    #TODO
-    pass
-
-def noecho():
-    #TODO
-    pass
-
-def meta(yes):
-    #TODO
-    pass
-
-def mousemask(mask):
-    # Mouse reporting - X10 compatbility mode
-    _wr(b"\x1b[?9h")
-
-def has_colors():
-    return False
-
-def can_change_color():
-    return False
-
-def curs_set(visibility):
-    if visibility > 0:
-        _wr(b"\x1b[?25h")
-    else:
-        _wr(b"\x1b[?25l")
-
-def beep():
-    _wr(b"\x07")
-
-def newwin(lines, cols, y=0, x=0):
-    #print("newwin(%d, %d, %d, %d)" % (lines, cols, y, x))
-    cols = cols or SCREEN.cols
-    lines = lines or SCREEN.lines
-    return Window(lines, cols, y, x)
diff --git a/unicodedata/metadata.txt b/unicodedata/metadata.txt
deleted file mode 100644
index dc5f60a66..000000000
--- a/unicodedata/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.1
diff --git a/unicodedata/setup.py b/unicodedata/setup.py
deleted file mode 100644
index 637bbc217..000000000
--- a/unicodedata/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-unicodedata',
-      version='0.0.1',
-      description='Dummy unicodedata module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['unicodedata'])
diff --git a/unicodedata/unicodedata.py b/unicodedata/unicodedata.py
deleted file mode 100644
index 6f041e408..000000000
--- a/unicodedata/unicodedata.py
+++ /dev/null
@@ -1,2 +0,0 @@
-def east_asian_width(c):
-    return 1
diff --git a/unittest/metadata.txt b/unittest/metadata.txt
deleted file mode 100644
index a984e65fe..000000000
--- a/unittest/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.2
diff --git a/unittest/setup.py b/unittest/setup.py
deleted file mode 100644
index 318fe8b07..000000000
--- a/unittest/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-unittest',
-      version='0.2',
-      description='unittest module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['unittest'])
diff --git a/unittest/test_unittest.py b/unittest/test_unittest.py
deleted file mode 100644
index 5c13b4432..000000000
--- a/unittest/test_unittest.py
+++ /dev/null
@@ -1,115 +0,0 @@
-import unittest
-
-class TestUnittestAssertions(unittest.TestCase):
-
-    def testFail(self):
-        with self.assertRaises(AssertionError):
-            self.fail('failure')
-
-    def testEqual(self):
-        self.assertEqual(0,0)
-        self.assertEqual([0,1,2], [0,1,2])
-        with self.assertRaises(AssertionError):
-            self.assertEqual(0,None)
-        with self.assertRaises(AssertionError):
-            self.assertEqual([0,1,2], [1,2,3])
-
-    def test_AlmostEqual(self):
-        self.assertAlmostEqual(    1.00000001, 1.0)
-        self.assertNotAlmostEqual( 1.0000001, 1.0)
-        with self.assertRaises(AssertionError):
-            self.assertAlmostEqual(1.0000001, 1.0)
-        with self.assertRaises(AssertionError):
-            self.assertNotAlmostEqual(1.00000001, 1.0)
-
-        self.assertAlmostEqual(1.1, 1.0, places=0)
-        with self.assertRaises(AssertionError):
-            self.assertAlmostEqual(1.1, 1.0, places=1)
-
-        self.assertAlmostEqual(0, .1+.1j, places=0)
-        self.assertNotAlmostEqual(0, .1+.1j, places=1)
-        with self.assertRaises(AssertionError):
-            self.assertAlmostEqual(0, .1+.1j, places=1)
-        with self.assertRaises(AssertionError):
-            self.assertNotAlmostEqual(0, .1+.1j, places=0)
-
-        self.assertAlmostEqual(float('inf'), float('inf'))
-        with self.assertRaises(AssertionError):
-            self.assertNotAlmostEqual(float('inf'), float('inf'))
-
-    def test_AmostEqualWithDelta(self):
-        self.assertAlmostEqual(1.1, 1.0, delta=0.5)
-        self.assertAlmostEqual(1.0, 1.1, delta=0.5)
-        self.assertNotAlmostEqual(1.1, 1.0, delta=0.05)
-        self.assertNotAlmostEqual(1.0, 1.1, delta=0.05)
-
-        self.assertAlmostEqual(1.0, 1.0, delta=0.5)
-        with self.assertRaises(AssertionError):
-            self.assertNotAlmostEqual(1.0, 1.0, delta=0.5)
-        with self.assertRaises(AssertionError):
-            self.assertAlmostEqual(1.1, 1.0, delta=0.05)
-        with self.assertRaises(AssertionError):
-            self.assertNotAlmostEqual(1.1, 1.0, delta=0.5)
-        with self.assertRaises(TypeError):
-            self.assertAlmostEqual(1.1, 1.0, places=2, delta=2)
-        with self.assertRaises(TypeError):
-            self.assertNotAlmostEqual(1.1, 1.0, places=2, delta=2)
-
-    def testNotEqual(self):
-        self.assertNotEqual([0,1,2], [0,2,1])
-        with self.assertRaises(AssertionError):
-            self.assertNotEqual(0,0)
-        with self.assertRaises(AssertionError):
-            self.assertNotEqual([0,1,2], [0,1,2])
-
-    def testIs(self):
-        self.assertIs(None, None)
-        with self.assertRaises(AssertionError):
-            self.assertIs([1,2,3], [1,2,3])
-
-    def testIsNot(self):
-        self.assertIsNot([1,2,3], [1,2,3])
-        with self.assertRaises(AssertionError):
-            self.assertIsNot(None, None)
-
-    def testIsNone(self):
-        self.assertIsNone(None)
-        with self.assertRaises(AssertionError):
-            self.assertIsNone(0)
-
-    def testIsNotNone(self):
-        self.assertIsNotNone(0)
-        with self.assertRaises(AssertionError):
-            self.assertIsNotNone(None)
-
-    def testTrue(self):
-        self.assertTrue(True)
-        with self.assertRaises(AssertionError):
-            self.assertTrue(False)
-
-    def testFalse(self):
-        self.assertFalse(False)
-        with self.assertRaises(AssertionError):
-            self.assertFalse(True)
-
-    def testIn(self):
-        self.assertIn('t', 'cat')
-        with self.assertRaises(AssertionError):
-            self.assertIn('x', 'cat')
-
-    def testIsInstance(self):
-        self.assertIsInstance('cat', str)
-        with self.assertRaises(AssertionError):
-            self.assertIsInstance(7, str)
-
-    def testRaises(self):
-        with self.assertRaises(ZeroDivisionError):
-            1/0
-            pass
-
-    @unittest.skip('test of skipping')
-    def testSkip(self):
-        self.assertFail('this should be skipped')
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/unittest/unittest.py b/unittest/unittest.py
deleted file mode 100644
index 48ef7126d..000000000
--- a/unittest/unittest.py
+++ /dev/null
@@ -1,202 +0,0 @@
-class SkipTest(Exception):
-    pass
-
-
-class AssertRaisesContext:
-
-    def __init__(self, exc):
-        self.expected = exc
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_value, tb):
-        if exc_type is None:
-            assert False, "%r not raised" % self.expected
-        if issubclass(exc_type, self.expected):
-            return True
-        return False
-
-
-class TestCase:
-
-    def fail(self, msg=''):
-        assert False, msg
-
-    def assertEqual(self, x, y, msg=''):
-        if not msg:
-            msg = "%r vs (expected) %r" % (x, y)
-        assert x == y, msg
-
-    def assertNotEqual(self, x, y, msg=''):
-        if not msg:
-            msg = "%r not expected to be equal %r" % (x, y)
-        assert x != y, msg
-
-    def assertAlmostEqual(self, x, y, places=None, msg='', delta=None):
-        if x == y:
-            return
-        if delta is not None and places is not None:
-            raise TypeError("specify delta or places not both")
-
-        if delta is not None:
-            if abs(x - y) <= delta:
-                return
-            if not msg:
-                msg = '%r != %r within %r delta' % (x, y, delta)
-        else:
-            if places is None:
-                places = 7
-            if round(abs(y-x), places) == 0:
-                return
-            if not msg:
-                msg = '%r != %r within %r places' % (x, y, places)
-
-        assert False, msg
-
-    def assertNotAlmostEqual(self, x, y, places=None, msg='', delta=None):
-        if delta is not None and places is not None:
-            raise TypeError("specify delta or places not both")
-
-        if delta is not None:
-            if not (x == y) and abs(x - y) > delta:
-                return
-            if not msg:
-                msg = '%r == %r within %r delta' % (x, y, delta)
-        else:
-            if places is None:
-                places = 7
-            if not (x == y) and round(abs(y-x), places) != 0:
-                return
-            if not msg:
-                msg = '%r == %r within %r places' % (x, y, places)
-
-        assert False, msg
-
-    def assertIs(self, x, y, msg=''):
-        if not msg:
-            msg = "%r is not %r" % (x, y)
-        assert x is y, msg
-
-    def assertIsNot(self, x, y, msg=''):
-        if not msg:
-            msg = "%r is %r" % (x, y)
-        assert x is not y, msg
-
-    def assertIsNone(self, x, msg=''):
-        if not msg:
-            msg = "%r is not None" % x
-        assert x is None, msg
-
-    def assertIsNotNone(self, x, msg=''):
-        if not msg:
-            msg = "%r is None" % x
-        assert x is not None, msg
-
-    def assertTrue(self, x, msg=''):
-        if not msg:
-            msg = "Expected %r to be True" % x
-        assert x, msg
-
-    def assertFalse(self, x, msg=''):
-        if not msg:
-            msg = "Expected %r to be False" % x
-        assert not x, msg
-
-    def assertIn(self, x, y, msg=''):
-        if not msg:
-            msg = "Expected %r to be in %r" % (x, y)
-        assert x in y, msg
-
-    def assertIsInstance(self, x, y, msg=''):
-        assert isinstance(x, y), msg
-
-    def assertRaises(self, exc, func=None, *args, **kwargs):
-        if func is None:
-            return AssertRaisesContext(exc)
-
-        try:
-            func(*args, **kwargs)
-            assert False, "%r not raised" % exc
-        except Exception as e:
-            if isinstance(e, exc):
-                return
-            raise
-
-
-
-def skip(msg):
-    def _decor(fun):
-        # We just replace original fun with _inner
-        def _inner(self):
-            raise SkipTest(msg)
-        return _inner
-    return _decor
-
-
-def skipUnless(cond, msg):
-    if cond:
-        return lambda x: x
-    return skip(msg)
-
-
-class TestSuite:
-    def __init__(self):
-        self.tests = []
-    def addTest(self, cls):
-        self.tests.append(cls)
-
-class TestRunner:
-    def run(self, suite):
-        res = TestResult()
-        for c in suite.tests:
-            run_class(c, res)
-        return res
-
-class TestResult:
-    def __init__(self):
-        self.errorsNum = 0
-        self.failuresNum = 0
-        self.skippedNum = 0
-        self.testsRun = 0
-
-    def wasSuccessful(self):
-        return self.errorsNum == 0 and self.failuresNum == 0
-
-# TODO: Uncompliant
-def run_class(c, test_result):
-    o = c()
-    set_up = getattr(o, "setUp", lambda: None)
-    tear_down = getattr(o, "tearDown", lambda: None)
-    for name in dir(o):
-        if name.startswith("test"):
-            print(name, end=' ...')
-            m = getattr(o, name)
-            try:
-                set_up()
-                test_result.testsRun += 1
-                m()
-                tear_down()
-                print(" ok")
-            except SkipTest as e:
-                print(" skipped:", e.args[0])
-                test_result.skippedNum += 1
-
-
-def main(module="__main__"):
-    def test_cases(m):
-        for tn in dir(m):
-            c = getattr(m, tn)
-            if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase):
-                yield c
-
-    m = __import__(module)
-    suite = TestSuite()
-    for c in test_cases(m):
-        suite.addTest(c)
-    runner = TestRunner()
-    result = runner.run(suite)
-    msg = "Ran %d tests" % result.testsRun
-    if result.skippedNum > 0:
-        msg += " (%d skipped)" % result.skippedNum
-    print(msg)
diff --git a/unix-ffi/README.md b/unix-ffi/README.md
new file mode 100644
index 000000000..6ea05d65a
--- /dev/null
+++ b/unix-ffi/README.md
@@ -0,0 +1,31 @@
+## Unix-specific packages
+
+These are packages that will only run on the Unix port of MicroPython, or are
+too big to be used on microcontrollers. There is some limited support for the
+Windows port too.
+
+**Note:** This directory is unmaintained.
+
+### Background
+
+The packages in this directory provide additional CPython compatibility using
+the host operating system's native libraries.
+
+This is implemented either by accessing the libraries directly via libffi, or
+by using built-in modules that are only available on the Unix port.
+
+In theory, this allows you to use MicroPython as a more complete drop-in
+replacement for CPython.
+
+### Usage
+
+To use a unix-specific library, a manifest file must add the `unix-ffi`
+library to the library search path using `add_library()`:
+
+```py
+add_library("unix-ffi", "$(MPY_LIB_DIR)/unix-ffi", prepend=True)
+```
+
+Prepending the `unix-ffi` library to the path will make it so that the
+`unix-ffi` version of a package will be preferred if that package appears in
+both `unix-ffi` and another library (eg `python-stdlib`).
diff --git a/_libc/_libc.py b/unix-ffi/_libc/_libc.py
similarity index 89%
rename from _libc/_libc.py
rename to unix-ffi/_libc/_libc.py
index a930cbf71..839b9d544 100644
--- a/_libc/_libc.py
+++ b/unix-ffi/_libc/_libc.py
@@ -4,7 +4,8 @@
 
 _h = None
 
-names = ('libc.so', 'libc.so.0', 'libc.so.6', 'libc.dylib')
+names = ("libc.so", "libc.so.0", "libc.so.6", "libc.dylib")
+
 
 def get():
     global _h
@@ -24,6 +25,7 @@ def set_names(n):
     global names
     names = n
 
+
 # Find out bitness of the platform, even if long ints are not supported
 # TODO: All bitness differences should be removed from micropython-lib, and
 # this snippet too.
diff --git a/unix-ffi/_libc/manifest.py b/unix-ffi/_libc/manifest.py
new file mode 100644
index 000000000..61bf421b6
--- /dev/null
+++ b/unix-ffi/_libc/manifest.py
@@ -0,0 +1,8 @@
+metadata(
+    description="MicroPython FFI helper module (deprecated, replaced by micropython-ffilib).",
+    version="0.3.1",
+)
+
+# Originally written by Paul Sokolovsky.
+
+module("_libc.py")
diff --git a/_markupbase/_markupbase.py b/unix-ffi/_markupbase/_markupbase.py
similarity index 83%
rename from _markupbase/_markupbase.py
rename to unix-ffi/_markupbase/_markupbase.py
index 2af5f1c23..7e1d91478 100644
--- a/_markupbase/_markupbase.py
+++ b/unix-ffi/_markupbase/_markupbase.py
@@ -7,15 +7,15 @@
 
 import re
 
-_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*').match
+_declname_match = re.compile(r"[a-zA-Z][-_.a-zA-Z0-9]*\s*").match
 _declstringlit_match = re.compile(r'(\'[^\']*\'|"[^"]*")\s*').match
-_commentclose = re.compile(r'--\s*>')
-_markedsectionclose = re.compile(r']\s*]\s*>')
+_commentclose = re.compile(r"--\s*>")
+_markedsectionclose = re.compile(r"]\s*]\s*>")
 
 # An analysis of the MS-Word extensions is available at
 # http://www.planetpublish.com/xmlarena/xap/Thursday/WordtoXML.pdf
 
-_msmarkedsectionclose = re.compile(r']\s*>')
+_msmarkedsectionclose = re.compile(r"]\s*>")
 
 del re
 
@@ -26,12 +26,10 @@ class ParserBase:
 
     def __init__(self):
         if self.__class__ is ParserBase:
-            raise RuntimeError(
-                "_markupbase.ParserBase must be subclassed")
+            raise RuntimeError("_markupbase.ParserBase must be subclassed")
 
     def error(self, message):
-        raise NotImplementedError(
-            "subclasses of ParserBase must override error()")
+        raise NotImplementedError("subclasses of ParserBase must override error()")
 
     def reset(self):
         self.lineno = 1
@@ -52,13 +50,13 @@ def updatepos(self, i, j):
         nlines = rawdata.count("\n", i, j)
         if nlines:
             self.lineno = self.lineno + nlines
-            pos = rawdata.rindex("\n", i, j) # Should not fail
-            self.offset = j-(pos+1)
+            pos = rawdata.rindex("\n", i, j)  # Should not fail
+            self.offset = j - (pos + 1)
         else:
-            self.offset = self.offset + j-i
+            self.offset = self.offset + j - i
         return j
 
-    _decl_otherchars = ''
+    _decl_otherchars = ""
 
     # Internal -- parse declaration (for use by subclasses).
     def parse_declaration(self, i):
@@ -75,35 +73,35 @@ def parse_declaration(self, i):
         rawdata = self.rawdata
         j = i + 2
         assert rawdata[i:j] == "<!", "unexpected call to parse_declaration"
-        if rawdata[j:j+1] == ">":
+        if rawdata[j : j + 1] == ">":
             # the empty comment <!>
             return j + 1
-        if rawdata[j:j+1] in ("-", ""):
+        if rawdata[j : j + 1] in ("-", ""):
             # Start of comment followed by buffer boundary,
             # or just a buffer boundary.
             return -1
         # A simple, practical version could look like: ((name|stringlit) S*) + '>'
         n = len(rawdata)
-        if rawdata[j:j+2] == '--': #comment
+        if rawdata[j : j + 2] == "--":  # comment
             # Locate --.*-- as the body of the comment
             return self.parse_comment(i)
-        elif rawdata[j] == '[': #marked section
+        elif rawdata[j] == "[":  # marked section
             # Locate [statusWord [...arbitrary SGML...]] as the body of the marked section
             # Where statusWord is one of TEMP, CDATA, IGNORE, INCLUDE, RCDATA
             # Note that this is extended by Microsoft Office "Save as Web" function
             # to include [if...] and [endif].
             return self.parse_marked_section(i)
-        else: #all other declaration elements
+        else:  # all other declaration elements
             decltype, j = self._scan_name(j, i)
         if j < 0:
             return j
         if decltype == "doctype":
-            self._decl_otherchars = ''
+            self._decl_otherchars = ""
         while j < n:
             c = rawdata[j]
             if c == ">":
                 # end of declaration syntax
-                data = rawdata[i+2:j]
+                data = rawdata[i + 2 : j]
                 if decltype == "doctype":
                     self.handle_decl(data)
                 else:
@@ -116,7 +114,7 @@ def parse_declaration(self, i):
             if c in "\"'":
                 m = _declstringlit_match(rawdata, j)
                 if not m:
-                    return -1 # incomplete
+                    return -1  # incomplete
                 j = m.end()
             elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
                 name, j = self._scan_name(j, i)
@@ -135,46 +133,45 @@ def parse_declaration(self, i):
                 else:
                     self.error("unexpected '[' char in declaration")
             else:
-                self.error(
-                    "unexpected %r char in declaration" % rawdata[j])
+                self.error("unexpected %r char in declaration" % rawdata[j])
             if j < 0:
                 return j
-        return -1 # incomplete
+        return -1  # incomplete
 
     # Internal -- parse a marked section
     # Override this to handle MS-word extension syntax <![if word]>content<![endif]>
     def parse_marked_section(self, i, report=1):
-        rawdata= self.rawdata
-        assert rawdata[i:i+3] == '<![', "unexpected call to parse_marked_section()"
-        sectName, j = self._scan_name( i+3, i )
+        rawdata = self.rawdata
+        assert rawdata[i : i + 3] == "<![", "unexpected call to parse_marked_section()"
+        sectName, j = self._scan_name(i + 3, i)
         if j < 0:
             return j
         if sectName in {"temp", "cdata", "ignore", "include", "rcdata"}:
             # look for standard ]]> ending
-            match= _markedsectionclose.search(rawdata, i+3)
+            match = _markedsectionclose.search(rawdata, i + 3)
         elif sectName in {"if", "else", "endif"}:
             # look for MS Office ]> ending
-            match= _msmarkedsectionclose.search(rawdata, i+3)
+            match = _msmarkedsectionclose.search(rawdata, i + 3)
         else:
-            self.error('unknown status keyword %r in marked section' % rawdata[i+3:j])
+            self.error("unknown status keyword %r in marked section" % rawdata[i + 3 : j])
         if not match:
             return -1
         if report:
             j = match.start(0)
-            self.unknown_decl(rawdata[i+3: j])
+            self.unknown_decl(rawdata[i + 3 : j])
         return match.end(0)
 
     # Internal -- parse comment, return length or -1 if not terminated
     def parse_comment(self, i, report=1):
         rawdata = self.rawdata
-        if rawdata[i:i+4] != '<!--':
-            self.error('unexpected call to parse_comment()')
-        match = _commentclose.search(rawdata, i+4)
+        if rawdata[i : i + 4] != "<!--":
+            self.error("unexpected call to parse_comment()")
+        match = _commentclose.search(rawdata, i + 4)
         if not match:
             return -1
         if report:
             j = match.start(0)
-            self.handle_comment(rawdata[i+4: j])
+            self.handle_comment(rawdata[i + 4 : j])
         return match.end(0)
 
     # Internal -- scan past the internal subset in a <!DOCTYPE declaration,
@@ -186,7 +183,7 @@ def _parse_doctype_subset(self, i, declstartpos):
         while j < n:
             c = rawdata[j]
             if c == "<":
-                s = rawdata[j:j+2]
+                s = rawdata[j : j + 2]
                 if s == "<":
                     # end of buffer; incomplete
                     return -1
@@ -199,7 +196,7 @@ def _parse_doctype_subset(self, i, declstartpos):
                 if (j + 4) > n:
                     # end of buffer; incomplete
                     return -1
-                if rawdata[j:j+4] == "<!--":
+                if rawdata[j : j + 4] == "<!--":
                     j = self.parse_comment(j, report=0)
                     if j < 0:
                         return j
@@ -209,8 +206,7 @@ def _parse_doctype_subset(self, i, declstartpos):
                     return -1
                 if name not in {"attlist", "element", "entity", "notation"}:
                     self.updatepos(declstartpos, j + 2)
-                    self.error(
-                        "unknown declaration %r in internal subset" % name)
+                    self.error("unknown declaration %r in internal subset" % name)
                 # handle the individual names
                 meth = getattr(self, "_parse_doctype_" + name)
                 j = meth(j, declstartpos)
@@ -252,7 +248,7 @@ def _parse_doctype_element(self, i, declstartpos):
             return -1
         # style content model; just skip until '>'
         rawdata = self.rawdata
-        if '>' in rawdata[j:]:
+        if ">" in rawdata[j:]:
             return rawdata.find(">", j) + 1
         return -1
 
@@ -260,7 +256,7 @@ def _parse_doctype_element(self, i, declstartpos):
     def _parse_doctype_attlist(self, i, declstartpos):
         rawdata = self.rawdata
         name, j = self._scan_name(i, declstartpos)
-        c = rawdata[j:j+1]
+        c = rawdata[j : j + 1]
         if c == "":
             return -1
         if c == ">":
@@ -271,7 +267,7 @@ def _parse_doctype_attlist(self, i, declstartpos):
             name, j = self._scan_name(j, declstartpos)
             if j < 0:
                 return j
-            c = rawdata[j:j+1]
+            c = rawdata[j : j + 1]
             if c == "":
                 return -1
             if c == "(":
@@ -280,14 +276,14 @@ def _parse_doctype_attlist(self, i, declstartpos):
                     j = rawdata.find(")", j) + 1
                 else:
                     return -1
-                while rawdata[j:j+1].isspace():
+                while rawdata[j : j + 1].isspace():
                     j = j + 1
                 if not rawdata[j:]:
                     # end of buffer, incomplete
                     return -1
             else:
                 name, j = self._scan_name(j, declstartpos)
-            c = rawdata[j:j+1]
+            c = rawdata[j : j + 1]
             if not c:
                 return -1
             if c in "'\"":
@@ -296,7 +292,7 @@ def _parse_doctype_attlist(self, i, declstartpos):
                     j = m.end()
                 else:
                     return -1
-                c = rawdata[j:j+1]
+                c = rawdata[j : j + 1]
                 if not c:
                     return -1
             if c == "#":
@@ -306,10 +302,10 @@ def _parse_doctype_attlist(self, i, declstartpos):
                 name, j = self._scan_name(j + 1, declstartpos)
                 if j < 0:
                     return j
-                c = rawdata[j:j+1]
+                c = rawdata[j : j + 1]
                 if not c:
                     return -1
-            if c == '>':
+            if c == ">":
                 # all done
                 return j + 1
 
@@ -320,11 +316,11 @@ def _parse_doctype_notation(self, i, declstartpos):
             return j
         rawdata = self.rawdata
         while 1:
-            c = rawdata[j:j+1]
+            c = rawdata[j : j + 1]
             if not c:
                 # end of buffer; incomplete
                 return -1
-            if c == '>':
+            if c == ">":
                 return j + 1
             if c in "'\"":
                 m = _declstringlit_match(rawdata, j)
@@ -339,10 +335,10 @@ def _parse_doctype_notation(self, i, declstartpos):
     # Internal -- scan past <!ENTITY declarations
     def _parse_doctype_entity(self, i, declstartpos):
         rawdata = self.rawdata
-        if rawdata[i:i+1] == "%":
+        if rawdata[i : i + 1] == "%":
             j = i + 1
             while 1:
-                c = rawdata[j:j+1]
+                c = rawdata[j : j + 1]
                 if not c:
                     return -1
                 if c.isspace():
@@ -355,7 +351,7 @@ def _parse_doctype_entity(self, i, declstartpos):
         if j < 0:
             return j
         while 1:
-            c = self.rawdata[j:j+1]
+            c = self.rawdata[j : j + 1]
             if not c:
                 return -1
             if c in "'\"":
@@ -363,7 +359,7 @@ def _parse_doctype_entity(self, i, declstartpos):
                 if m:
                     j = m.end()
                 else:
-                    return -1    # incomplete
+                    return -1  # incomplete
             elif c == ">":
                 return j + 1
             else:
@@ -387,8 +383,7 @@ def _scan_name(self, i, declstartpos):
             return name.lower(), m.end()
         else:
             self.updatepos(declstartpos, i)
-            self.error("expected name token at %r"
-                       % rawdata[declstartpos:declstartpos+20])
+            self.error("expected name token at %r" % rawdata[declstartpos : declstartpos + 20])
 
     # To be overridden -- handlers for unknown objects
     def unknown_decl(self, data):
diff --git a/unix-ffi/_markupbase/manifest.py b/unix-ffi/_markupbase/manifest.py
new file mode 100644
index 000000000..9cbf52bb0
--- /dev/null
+++ b/unix-ffi/_markupbase/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="3.3.4")
+
+require("re")
+
+module("_markupbase.py")
diff --git a/cgi/cgi.py b/unix-ffi/cgi/cgi.py
similarity index 77%
rename from cgi/cgi.py
rename to unix-ffi/cgi/cgi.py
index dbe44cc8e..550f70713 100644
--- a/cgi/cgi.py
+++ b/unix-ffi/cgi/cgi.py
@@ -25,9 +25,6 @@
 # responsible for its maintenance.
 #
 
-__version__ = "2.6"
-
-
 # Imports
 # =======
 
@@ -41,17 +38,29 @@
 import locale
 import tempfile
 
-__all__ = ["MiniFieldStorage", "FieldStorage",
-           "parse", "parse_qs", "parse_qsl", "parse_multipart",
-           "parse_header", "print_exception", "print_environ",
-           "print_form", "print_directory", "print_arguments",
-           "print_environ_usage", "escape"]
+__all__ = [
+    "MiniFieldStorage",
+    "FieldStorage",
+    "parse",
+    "parse_qs",
+    "parse_qsl",
+    "parse_multipart",
+    "parse_header",
+    "print_exception",
+    "print_environ",
+    "print_form",
+    "print_directory",
+    "print_arguments",
+    "print_environ_usage",
+    "escape",
+]
 
 # Logging support
 # ===============
 
-logfile = ""            # Filename to log to, if not empty
-logfp = None            # File object to log to, if not None
+logfile = ""  # Filename to log to, if not empty
+logfp = None  # File object to log to, if not None
+
 
 def initlog(*allargs):
     """Write a log message, if there is a log file.
@@ -88,24 +97,28 @@ def initlog(*allargs):
         log = dolog
     log(*allargs)
 
+
 def dolog(fmt, *args):
     """Write a log message to the log file.  See initlog() for docs."""
-    logfp.write(fmt%args + "\n")
+    logfp.write(fmt % args + "\n")
+
 
 def nolog(*allargs):
     """Dummy function, assigned to log when logging is disabled."""
     pass
 
+
 def closelog():
     """Close the log file."""
     global log, logfile, logfp
-    logfile = ''
+    logfile = ""
     if logfp:
         logfp.close()
         logfp = None
     log = initlog
 
-log = initlog           # The current logging function
+
+log = initlog  # The current logging function
 
 
 # Parsing functions
@@ -115,87 +128,90 @@ def closelog():
 # 0 ==> unlimited input
 maxlen = 0
 
+
 def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
     """Parse a query in the environment or from a file (default stdin)
 
-        Arguments, all optional:
+    Arguments, all optional:
 
-        fp              : file pointer; default: sys.stdin.buffer
+    fp              : file pointer; default: sys.stdin.buffer
 
-        environ         : environment dictionary; default: os.environ
+    environ         : environment dictionary; default: os.environ
 
-        keep_blank_values: flag indicating whether blank values in
-            percent-encoded forms should be treated as blank strings.
-            A true value indicates that blanks should be retained as
-            blank strings.  The default false value indicates that
-            blank values are to be ignored and treated as if they were
-            not included.
+    keep_blank_values: flag indicating whether blank values in
+        percent-encoded forms should be treated as blank strings.
+        A true value indicates that blanks should be retained as
+        blank strings.  The default false value indicates that
+        blank values are to be ignored and treated as if they were
+        not included.
 
-        strict_parsing: flag indicating what to do with parsing errors.
-            If false (the default), errors are silently ignored.
-            If true, errors raise a ValueError exception.
+    strict_parsing: flag indicating what to do with parsing errors.
+        If false (the default), errors are silently ignored.
+        If true, errors raise a ValueError exception.
     """
     if fp is None:
         fp = sys.stdin
 
     # field keys and values (except for files) are returned as strings
     # an encoding is required to decode the bytes read from self.fp
-    if hasattr(fp,'encoding'):
+    if hasattr(fp, "encoding"):
         encoding = fp.encoding
     else:
-        encoding = 'latin-1'
+        encoding = "latin-1"
 
     # fp.read() must return bytes
     if isinstance(fp, TextIOWrapper):
         fp = fp.buffer
 
-    if not 'REQUEST_METHOD' in environ:
-        environ['REQUEST_METHOD'] = 'GET'       # For testing stand-alone
-    if environ['REQUEST_METHOD'] == 'POST':
-        ctype, pdict = parse_header(environ['CONTENT_TYPE'])
-        if ctype == 'multipart/form-data':
+    if not "REQUEST_METHOD" in environ:
+        environ["REQUEST_METHOD"] = "GET"  # For testing stand-alone
+    if environ["REQUEST_METHOD"] == "POST":
+        ctype, pdict = parse_header(environ["CONTENT_TYPE"])
+        if ctype == "multipart/form-data":
             return parse_multipart(fp, pdict)
-        elif ctype == 'application/x-www-form-urlencoded':
-            clength = int(environ['CONTENT_LENGTH'])
+        elif ctype == "application/x-www-form-urlencoded":
+            clength = int(environ["CONTENT_LENGTH"])
             if maxlen and clength > maxlen:
-                raise ValueError('Maximum content length exceeded')
+                raise ValueError("Maximum content length exceeded")
             qs = fp.read(clength).decode(encoding)
         else:
-            qs = ''                     # Unknown content-type
-        if 'QUERY_STRING' in environ:
-            if qs: qs = qs + '&'
-            qs = qs + environ['QUERY_STRING']
+            qs = ""  # Unknown content-type
+        if "QUERY_STRING" in environ:
+            if qs:
+                qs = qs + "&"
+            qs = qs + environ["QUERY_STRING"]
         elif sys.argv[1:]:
-            if qs: qs = qs + '&'
+            if qs:
+                qs = qs + "&"
             qs = qs + sys.argv[1]
-        environ['QUERY_STRING'] = qs    # XXX Shouldn't, really
-    elif 'QUERY_STRING' in environ:
-        qs = environ['QUERY_STRING']
+        environ["QUERY_STRING"] = qs  # XXX Shouldn't, really
+    elif "QUERY_STRING" in environ:
+        qs = environ["QUERY_STRING"]
     else:
         if sys.argv[1:]:
             qs = sys.argv[1]
         else:
             qs = ""
-        environ['QUERY_STRING'] = qs    # XXX Shouldn't, really
-    return urllib.parse.parse_qs(qs, keep_blank_values, strict_parsing,
-                                 encoding=encoding)
+        environ["QUERY_STRING"] = qs  # XXX Shouldn't, really
+    return urllib.parse.parse_qs(qs, keep_blank_values, strict_parsing, encoding=encoding)
 
 
 # parse query string function called from urlparse,
 # this is done in order to maintain backward compatiblity.
 
+
 def parse_qs(qs, keep_blank_values=0, strict_parsing=0):
     """Parse a query given as a string argument."""
-    warn("cgi.parse_qs is deprecated, use urllib.parse.parse_qs instead",
-         DeprecationWarning, 2)
+    warn("cgi.parse_qs is deprecated, use urllib.parse.parse_qs instead", DeprecationWarning, 2)
     return urllib.parse.parse_qs(qs, keep_blank_values, strict_parsing)
 
+
 def parse_qsl(qs, keep_blank_values=0, strict_parsing=0):
     """Parse a query given as a string argument."""
-    warn("cgi.parse_qsl is deprecated, use urllib.parse.parse_qsl instead",
-         DeprecationWarning, 2)
+    warn("cgi.parse_qsl is deprecated, use urllib.parse.parse_qsl instead", DeprecationWarning, 2)
     return urllib.parse.parse_qsl(qs, keep_blank_values, strict_parsing)
 
+
 def parse_multipart(fp, pdict):
     """Parse multipart input.
 
@@ -224,11 +240,10 @@ def parse_multipart(fp, pdict):
     import http.client
 
     boundary = b""
-    if 'boundary' in pdict:
-        boundary = pdict['boundary']
+    if "boundary" in pdict:
+        boundary = pdict["boundary"]
     if not valid_boundary(boundary):
-        raise ValueError('Invalid boundary in multipart form: %r'
-                            % (boundary,))
+        raise ValueError("Invalid boundary in multipart form: %r" % (boundary,))
 
     nextpart = b"--" + boundary
     lastpart = b"--" + boundary + b"--"
@@ -241,7 +256,7 @@ def parse_multipart(fp, pdict):
         if terminator:
             # At start of next part.  Read headers first.
             headers = http.client.parse_headers(fp)
-            clength = headers.get('content-length')
+            clength = headers.get("content-length")
             if clength:
                 try:
                     bytes = int(clength)
@@ -249,7 +264,7 @@ def parse_multipart(fp, pdict):
                     pass
             if bytes > 0:
                 if maxlen and bytes > maxlen:
-                    raise ValueError('Maximum content length exceeded')
+                    raise ValueError("Maximum content length exceeded")
                 data = fp.read(bytes)
             else:
                 data = b""
@@ -258,7 +273,7 @@ def parse_multipart(fp, pdict):
         while 1:
             line = fp.readline()
             if not line:
-                terminator = lastpart # End outer loop
+                terminator = lastpart  # End outer loop
                 break
             if line.startswith(b"--"):
                 terminator = line.rstrip()
@@ -278,14 +293,14 @@ def parse_multipart(fp, pdict):
                     line = line[:-1]
                 lines[-1] = line
                 data = b"".join(lines)
-        line = headers['content-disposition']
+        line = headers["content-disposition"]
         if not line:
             continue
         key, params = parse_header(line)
-        if key != 'form-data':
+        if key != "form-data":
             continue
-        if 'name' in params:
-            name = params['name']
+        if "name" in params:
+            name = params["name"]
         else:
             continue
         if name in partdict:
@@ -297,34 +312,35 @@ def parse_multipart(fp, pdict):
 
 
 def _parseparam(s):
-    while s[:1] == ';':
+    while s[:1] == ";":
         s = s[1:]
-        end = s.find(';')
+        end = s.find(";")
         while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
-            end = s.find(';', end + 1)
+            end = s.find(";", end + 1)
         if end < 0:
             end = len(s)
         f = s[:end]
         yield f.strip()
         s = s[end:]
 
+
 def parse_header(line):
     """Parse a Content-type like header.
 
     Return the main content-type and a dictionary of options.
 
     """
-    parts = _parseparam(';' + line)
+    parts = _parseparam(";" + line)
     key = parts.__next__()
     pdict = {}
     for p in parts:
-        i = p.find('=')
+        i = p.find("=")
         if i >= 0:
             name = p[:i].strip().lower()
-            value = p[i+1:].strip()
+            value = p[i + 1 :].strip()
             if len(value) >= 2 and value[0] == value[-1] == '"':
                 value = value[1:-1]
-                value = value.replace('\\\\', '\\').replace('\\"', '"')
+                value = value.replace("\\\\", "\\").replace('\\"', '"')
             pdict[name] = value
     return key, pdict
 
@@ -332,6 +348,7 @@ def parse_header(line):
 # Classes for field storage
 # =========================
 
+
 class MiniFieldStorage:
 
     """Like FieldStorage, for use when no file uploads are possible."""
@@ -400,9 +417,19 @@ class FieldStorage:
     directory and unlinking them as soon as they have been opened.
 
     """
-    def __init__(self, fp=None, headers=None, outerboundary=b'',
-                 environ=os.environ, keep_blank_values=0, strict_parsing=0,
-                 limit=None, encoding='utf-8', errors='replace'):
+
+    def __init__(
+        self,
+        fp=None,
+        headers=None,
+        outerboundary=b"",
+        environ=os.environ,
+        keep_blank_values=0,
+        strict_parsing=0,
+        limit=None,
+        encoding="utf-8",
+        errors="replace",
+    ):
         """Constructor.  Read multipart/* until last part.
 
         Arguments, all optional:
@@ -443,35 +470,34 @@ def __init__(self, fp=None, headers=None, outerboundary=b'',
             header)
 
         """
-        method = 'GET'
+        method = "GET"
         self.keep_blank_values = keep_blank_values
         self.strict_parsing = strict_parsing
-        if 'REQUEST_METHOD' in environ:
-            method = environ['REQUEST_METHOD'].upper()
+        if "REQUEST_METHOD" in environ:
+            method = environ["REQUEST_METHOD"].upper()
         self.qs_on_post = None
-        if method == 'GET' or method == 'HEAD':
-            if 'QUERY_STRING' in environ:
-                qs = environ['QUERY_STRING']
+        if method == "GET" or method == "HEAD":
+            if "QUERY_STRING" in environ:
+                qs = environ["QUERY_STRING"]
             elif sys.argv[1:]:
                 qs = sys.argv[1]
             else:
                 qs = ""
-            qs = qs.encode(locale.getpreferredencoding(), 'surrogateescape')
+            qs = qs.encode(locale.getpreferredencoding(), "surrogateescape")
             fp = BytesIO(qs)
             if headers is None:
-                headers = {'content-type':
-                           "application/x-www-form-urlencoded"}
+                headers = {"content-type": "application/x-www-form-urlencoded"}
         if headers is None:
             headers = {}
-            if method == 'POST':
+            if method == "POST":
                 # Set default content-type for POST to what's traditional
-                headers['content-type'] = "application/x-www-form-urlencoded"
-            if 'CONTENT_TYPE' in environ:
-                headers['content-type'] = environ['CONTENT_TYPE']
-            if 'QUERY_STRING' in environ:
-                self.qs_on_post = environ['QUERY_STRING']
-            if 'CONTENT_LENGTH' in environ:
-                headers['content-length'] = environ['CONTENT_LENGTH']
+                headers["content-type"] = "application/x-www-form-urlencoded"
+            if "CONTENT_TYPE" in environ:
+                headers["content-type"] = environ["CONTENT_TYPE"]
+            if "QUERY_STRING" in environ:
+                self.qs_on_post = environ["QUERY_STRING"]
+            if "CONTENT_LENGTH" in environ:
+                headers["content-length"] = environ["CONTENT_LENGTH"]
         if fp is None:
             self.fp = sys.stdin.buffer
         # self.fp.read() must return bytes
@@ -485,8 +511,7 @@ def __init__(self, fp=None, headers=None, outerboundary=b'',
 
         self.headers = headers
         if not isinstance(outerboundary, bytes):
-            raise TypeError('outerboundary must be bytes, not %s'
-                            % type(outerboundary).__name__)
+            raise TypeError("outerboundary must be bytes, not %s" % type(outerboundary).__name__)
         self.outerboundary = outerboundary
 
         self.bytes_read = 0
@@ -494,16 +519,16 @@ def __init__(self, fp=None, headers=None, outerboundary=b'',
 
         # Process content-disposition header
         cdisp, pdict = "", {}
-        if 'content-disposition' in self.headers:
-            cdisp, pdict = parse_header(self.headers['content-disposition'])
+        if "content-disposition" in self.headers:
+            cdisp, pdict = parse_header(self.headers["content-disposition"])
         self.disposition = cdisp
         self.disposition_options = pdict
         self.name = None
-        if 'name' in pdict:
-            self.name = pdict['name']
+        if "name" in pdict:
+            self.name = pdict["name"]
         self.filename = None
-        if 'filename' in pdict:
-            self.filename = pdict['filename']
+        if "filename" in pdict:
+            self.filename = pdict["filename"]
         self._binary_file = self.filename is not None
 
         # Process content-type header
@@ -518,50 +543,49 @@ def __init__(self, fp=None, headers=None, outerboundary=b'',
         #
         # See below for what we do if there does exist a content-type header,
         # but it happens to be something we don't understand.
-        if 'content-type' in self.headers:
-            ctype, pdict = parse_header(self.headers['content-type'])
-        elif self.outerboundary or method != 'POST':
+        if "content-type" in self.headers:
+            ctype, pdict = parse_header(self.headers["content-type"])
+        elif self.outerboundary or method != "POST":
             ctype, pdict = "text/plain", {}
         else:
-            ctype, pdict = 'application/x-www-form-urlencoded', {}
+            ctype, pdict = "application/x-www-form-urlencoded", {}
         self.type = ctype
         self.type_options = pdict
-        if 'boundary' in pdict:
-            self.innerboundary = pdict['boundary'].encode(self.encoding)
+        if "boundary" in pdict:
+            self.innerboundary = pdict["boundary"].encode(self.encoding)
         else:
             self.innerboundary = b""
 
         clen = -1
-        if 'content-length' in self.headers:
+        if "content-length" in self.headers:
             try:
-                clen = int(self.headers['content-length'])
+                clen = int(self.headers["content-length"])
             except ValueError:
                 pass
             if maxlen and clen > maxlen:
-                raise ValueError('Maximum content length exceeded')
+                raise ValueError("Maximum content length exceeded")
         self.length = clen
         if self.limit is None and clen:
             self.limit = clen
 
         self.list = self.file = None
         self.done = 0
-        if ctype == 'application/x-www-form-urlencoded':
+        if ctype == "application/x-www-form-urlencoded":
             self.read_urlencoded()
-        elif ctype[:10] == 'multipart/':
+        elif ctype[:10] == "multipart/":
             self.read_multi(environ, keep_blank_values, strict_parsing)
         else:
             self.read_single()
 
     def __repr__(self):
         """Return a printable representation."""
-        return "FieldStorage(%r, %r, %r)" % (
-                self.name, self.filename, self.value)
+        return "FieldStorage(%r, %r, %r)" % (self.name, self.filename, self.value)
 
     def __iter__(self):
         return iter(self.keys())
 
     def __getattr__(self, name):
-        if name != 'value':
+        if name != "value":
             raise AttributeError(name)
         if self.file:
             self.file.seek(0)
@@ -579,7 +603,8 @@ def __getitem__(self, key):
             raise TypeError("not indexable")
         found = []
         for item in self.list:
-            if item.name == key: found.append(item)
+            if item.name == key:
+                found.append(item)
         if not found:
             raise KeyError(key)
         if len(found) == 1:
@@ -599,7 +624,7 @@ def getvalue(self, key, default=None):
             return default
 
     def getfirst(self, key, default=None):
-        """ Return the first value received."""
+        """Return the first value received."""
         if key in self:
             value = self[key]
             if isinstance(value, list):
@@ -610,7 +635,7 @@ def getfirst(self, key, default=None):
             return default
 
     def getlist(self, key):
-        """ Return list of received values."""
+        """Return list of received values."""
         if key in self:
             value = self[key]
             if isinstance(value, list):
@@ -643,15 +668,18 @@ def read_urlencoded(self):
         """Internal: read data in query string format."""
         qs = self.fp.read(self.length)
         if not isinstance(qs, bytes):
-            raise ValueError("%s should return bytes, got %s" \
-                             % (self.fp, type(qs).__name__))
+            raise ValueError("%s should return bytes, got %s" % (self.fp, type(qs).__name__))
         qs = qs.decode(self.encoding, self.errors)
         if self.qs_on_post:
-            qs += '&' + self.qs_on_post
+            qs += "&" + self.qs_on_post
         self.list = []
         query = urllib.parse.parse_qsl(
-            qs, self.keep_blank_values, self.strict_parsing,
-            encoding=self.encoding, errors=self.errors)
+            qs,
+            self.keep_blank_values,
+            self.strict_parsing,
+            encoding=self.encoding,
+            errors=self.errors,
+        )
         for key, value in query:
             self.list.append(MiniFieldStorage(key, value))
         self.skip_lines()
@@ -662,21 +690,26 @@ def read_multi(self, environ, keep_blank_values, strict_parsing):
         """Internal: read a part that is itself multipart."""
         ib = self.innerboundary
         if not valid_boundary(ib):
-            raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
+            raise ValueError("Invalid boundary in multipart form: %r" % (ib,))
         self.list = []
         if self.qs_on_post:
             query = urllib.parse.parse_qsl(
-                self.qs_on_post, self.keep_blank_values, self.strict_parsing,
-                encoding=self.encoding, errors=self.errors)
+                self.qs_on_post,
+                self.keep_blank_values,
+                self.strict_parsing,
+                encoding=self.encoding,
+                errors=self.errors,
+            )
             for key, value in query:
                 self.list.append(MiniFieldStorage(key, value))
             FieldStorageClass = None
 
         klass = self.FieldStorageClass or self.__class__
-        first_line = self.fp.readline() # bytes
+        first_line = self.fp.readline()  # bytes
         if not isinstance(first_line, bytes):
-            raise ValueError("%s should return bytes, got %s" \
-                             % (self.fp, type(first_line).__name__))
+            raise ValueError(
+                "%s should return bytes, got %s" % (self.fp, type(first_line).__name__)
+            )
         self.bytes_read += len(first_line)
         # first line holds boundary ; ignore it, or check that
         # b"--" + ib == first_line.strip() ?
@@ -694,9 +727,17 @@ def read_multi(self, environ, keep_blank_values, strict_parsing):
             self.bytes_read += len(hdr_text)
             parser.feed(hdr_text.decode(self.encoding, self.errors))
             headers = parser.close()
-            part = klass(self.fp, headers, ib, environ, keep_blank_values,
-                         strict_parsing,self.limit-self.bytes_read,
-                         self.encoding, self.errors)
+            part = klass(
+                self.fp,
+                headers,
+                ib,
+                environ,
+                keep_blank_values,
+                strict_parsing,
+                self.limit - self.bytes_read,
+                self.encoding,
+                self.errors,
+            )
             self.bytes_read += part.bytes_read
             self.list.append(part)
             if part.done or self.bytes_read >= self.length > 0:
@@ -712,7 +753,7 @@ def read_single(self):
             self.read_lines()
         self.file.seek(0)
 
-    bufsize = 8*1024            # I/O buffering size for copy to file
+    bufsize = 8 * 1024  # I/O buffering size for copy to file
 
     def read_binary(self):
         """Internal: read binary data."""
@@ -720,10 +761,11 @@ def read_binary(self):
         todo = self.length
         if todo >= 0:
             while todo > 0:
-                data = self.fp.read(min(todo, self.bufsize)) # bytes
+                data = self.fp.read(min(todo, self.bufsize))  # bytes
                 if not isinstance(data, bytes):
-                    raise ValueError("%s should return bytes, got %s"
-                                     % (self.fp, type(data).__name__))
+                    raise ValueError(
+                        "%s should return bytes, got %s" % (self.fp, type(data).__name__)
+                    )
                 self.bytes_read += len(data)
                 if not data:
                     self.done = -1
@@ -734,9 +776,9 @@ def read_binary(self):
     def read_lines(self):
         """Internal: read lines until EOF or outerboundary."""
         if self._binary_file:
-            self.file = self.__file = BytesIO() # store data as bytes for files
+            self.file = self.__file = BytesIO()  # store data as bytes for files
         else:
-            self.file = self.__file = StringIO() # as strings for other fields
+            self.file = self.__file = StringIO()  # as strings for other fields
         if self.outerboundary:
             self.read_lines_to_outerboundary()
         else:
@@ -760,7 +802,7 @@ def __write(self, line):
     def read_lines_to_eof(self):
         """Internal: read lines until EOF."""
         while 1:
-            line = self.fp.readline(1<<16) # bytes
+            line = self.fp.readline(1 << 16)  # bytes
             self.bytes_read += len(line)
             if not line:
                 self.done = -1
@@ -780,7 +822,7 @@ def read_lines_to_outerboundary(self):
         while 1:
             if _read >= self.limit:
                 break
-            line = self.fp.readline(1<<16) # bytes
+            line = self.fp.readline(1 << 16)  # bytes
             self.bytes_read += len(line)
             _read += len(line)
             if not line:
@@ -824,7 +866,7 @@ def skip_lines(self):
         last_boundary = next_boundary + b"--"
         last_line_lfend = True
         while True:
-            line = self.fp.readline(1<<16)
+            line = self.fp.readline(1 << 16)
             self.bytes_read += len(line)
             if not line:
                 self.done = -1
@@ -836,7 +878,7 @@ def skip_lines(self):
                 if strippedline == last_boundary:
                     self.done = 1
                     break
-            last_line_lfend = line.endswith(b'\n')
+            last_line_lfend = line.endswith(b"\n")
 
     def make_file(self):
         """Overridable: return a readable & writable file.
@@ -865,13 +907,13 @@ def make_file(self):
         if self._binary_file:
             return tempfile.TemporaryFile("wb+")
         else:
-            return tempfile.TemporaryFile("w+",
-                encoding=self.encoding, newline = '\n')
+            return tempfile.TemporaryFile("w+", encoding=self.encoding, newline="\n")
 
 
 # Test/debug code
 # ===============
 
+
 def test(environ=os.environ):
     """Robust test CGI script, usable as main program.
 
@@ -881,18 +923,21 @@ def test(environ=os.environ):
     """
     print("Content-type: text/html")
     print()
-    #sys.stderr = sys.stdout
+    # sys.stderr = sys.stdout
     try:
-        form = FieldStorage()   # Replace with other classes to test those
+        form = FieldStorage()  # Replace with other classes to test those
         print_directory()
         print_arguments()
         print_form(form)
         print_environ(environ)
         print_environ_usage()
+
         def f():
             exec("testing print_exception() -- <I>italics?</I>")
+
         def g(f=f):
             f()
+
         print("<H3>What follows is a test, not an actual exception:</H3>")
         g()
     except:
@@ -903,7 +948,7 @@ def g(f=f):
     global maxlen
     maxlen = 50
     try:
-        form = FieldStorage()   # Replace with other classes to test those
+        form = FieldStorage()  # Replace with other classes to test those
         print_directory()
         print_arguments()
         print_form(form)
@@ -911,20 +956,25 @@ def g(f=f):
     except:
         print_exception()
 
+
 def print_exception(type=None, value=None, tb=None, limit=None):
     if type is None:
         type, value, tb = sys.exc_info()
     import traceback
+
     print()
     print("<H3>Traceback (most recent call last):</H3>")
-    list = traceback.format_tb(tb, limit) + \
-           traceback.format_exception_only(type, value)
-    print("<PRE>%s<B>%s</B></PRE>" % (
-        html.escape("".join(list[:-1])),
-        html.escape(list[-1]),
-        ))
+    list = traceback.format_tb(tb, limit) + traceback.format_exception_only(type, value)
+    print(
+        "<PRE>%s<B>%s</B></PRE>"
+        % (
+            html.escape("".join(list[:-1])),
+            html.escape(list[-1]),
+        )
+    )
     del tb
 
+
 def print_environ(environ=os.environ):
     """Dump the shell environment as HTML."""
     keys = sorted(environ.keys())
@@ -936,6 +986,7 @@ def print_environ(environ=os.environ):
     print("</DL>")
     print()
 
+
 def print_form(form):
     """Dump the contents of a form as HTML."""
     keys = sorted(form.keys())
@@ -945,13 +996,14 @@ def print_form(form):
         print("<P>No form fields.")
     print("<DL>")
     for key in keys:
-        print("<DT>" + html.escape(key) + ":", end=' ')
+        print("<DT>" + html.escape(key) + ":", end=" ")
         value = form[key]
         print("<i>" + html.escape(repr(type(value))) + "</i>")
         print("<DD>" + html.escape(repr(value)))
     print("</DL>")
     print()
 
+
 def print_directory():
     """Dump the current directory as HTML."""
     print()
@@ -964,6 +1016,7 @@ def print_directory():
         print(html.escape(pwd))
     print()
 
+
 def print_arguments():
     print()
     print("<H3>Command Line Arguments:</H3>")
@@ -971,9 +1024,11 @@ def print_arguments():
     print(sys.argv)
     print()
 
+
 def print_environ_usage():
     """Dump a list of environment variables used by CGI as HTML."""
-    print("""
+    print(
+        """
 <H3>These environment variables could have been set:</H3>
 <UL>
 <LI>AUTH_TYPE
@@ -1012,17 +1067,18 @@ def print_environ_usage():
 <LI>HTTP_REFERER
 <LI>HTTP_USER_AGENT
 </UL>
-""")
+"""
+    )
 
 
 # Utilities
 # =========
 
+
 def escape(s, quote=None):
     """Deprecated API."""
-    warn("cgi.escape is deprecated, use html.escape instead",
-         DeprecationWarning, stacklevel=2)
-    s = s.replace("&", "&amp;") # Must be done first!
+    warn("cgi.escape is deprecated, use html.escape instead", DeprecationWarning, stacklevel=2)
+    s = s.replace("&", "&amp;")  # Must be done first!
     s = s.replace("<", "&lt;")
     s = s.replace(">", "&gt;")
     if quote:
@@ -1032,15 +1088,17 @@ def escape(s, quote=None):
 
 def valid_boundary(s, _vb_pattern=None):
     import re
+
     if isinstance(s, bytes):
         _vb_pattern = b"^[ -~]{0,200}[!-~]$"
     else:
         _vb_pattern = "^[ -~]{0,200}[!-~]$"
     return re.match(_vb_pattern, s)
 
+
 # Invoke mainline
 # ===============
 
 # Call test() when this file is run as a script (not imported as a module)
-if __name__ == '__main__':
+if __name__ == "__main__":
     test()
diff --git a/unix-ffi/cgi/manifest.py b/unix-ffi/cgi/manifest.py
new file mode 100644
index 000000000..29732c939
--- /dev/null
+++ b/unix-ffi/cgi/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="3.3.4")
+
+module("cgi.py")
diff --git a/email.charset/email/charset.py b/unix-ffi/email.charset/email/charset.py
similarity index 83%
rename from email.charset/email/charset.py
rename to unix-ffi/email.charset/email/charset.py
index 892bab54a..304cc8fef 100644
--- a/email.charset/email/charset.py
+++ b/unix-ffi/email.charset/email/charset.py
@@ -3,11 +3,11 @@
 # Contact: email-sig@python.org
 
 __all__ = [
-    'Charset',
-    'add_alias',
-    'add_charset',
-    'add_codec',
-    ]
+    "Charset",
+    "add_alias",
+    "add_charset",
+    "add_codec",
+]
 
 from functools import partial
 
@@ -18,93 +18,90 @@
 from email.encoders import encode_7or8bit
 
 
-
 # Flags for types of header encodings
-QP          = 1 # Quoted-Printable
-BASE64      = 2 # Base64
-SHORTEST    = 3 # the shorter of QP and base64, but only for headers
+QP = 1  # Quoted-Printable
+BASE64 = 2  # Base64
+SHORTEST = 3  # the shorter of QP and base64, but only for headers
 
 # In "=?charset?q?hello_world?=", the =?, ?q?, and ?= add up to 7
 RFC2047_CHROME_LEN = 7
 
-DEFAULT_CHARSET = 'us-ascii'
-UNKNOWN8BIT = 'unknown-8bit'
-EMPTYSTRING = ''
+DEFAULT_CHARSET = "us-ascii"
+UNKNOWN8BIT = "unknown-8bit"
+EMPTYSTRING = ""
 
 
-
 # Defaults
 CHARSETS = {
     # input        header enc  body enc output conv
-    'iso-8859-1':  (QP,        QP,      None),
-    'iso-8859-2':  (QP,        QP,      None),
-    'iso-8859-3':  (QP,        QP,      None),
-    'iso-8859-4':  (QP,        QP,      None),
+    "iso-8859-1": (QP, QP, None),
+    "iso-8859-2": (QP, QP, None),
+    "iso-8859-3": (QP, QP, None),
+    "iso-8859-4": (QP, QP, None),
     # iso-8859-5 is Cyrillic, and not especially used
     # iso-8859-6 is Arabic, also not particularly used
     # iso-8859-7 is Greek, QP will not make it readable
     # iso-8859-8 is Hebrew, QP will not make it readable
-    'iso-8859-9':  (QP,        QP,      None),
-    'iso-8859-10': (QP,        QP,      None),
+    "iso-8859-9": (QP, QP, None),
+    "iso-8859-10": (QP, QP, None),
     # iso-8859-11 is Thai, QP will not make it readable
-    'iso-8859-13': (QP,        QP,      None),
-    'iso-8859-14': (QP,        QP,      None),
-    'iso-8859-15': (QP,        QP,      None),
-    'iso-8859-16': (QP,        QP,      None),
-    'windows-1252':(QP,        QP,      None),
-    'viscii':      (QP,        QP,      None),
-    'us-ascii':    (None,      None,    None),
-    'big5':        (BASE64,    BASE64,  None),
-    'gb2312':      (BASE64,    BASE64,  None),
-    'euc-jp':      (BASE64,    None,    'iso-2022-jp'),
-    'shift_jis':   (BASE64,    None,    'iso-2022-jp'),
-    'iso-2022-jp': (BASE64,    None,    None),
-    'koi8-r':      (BASE64,    BASE64,  None),
-    'utf-8':       (SHORTEST,  BASE64, 'utf-8'),
-    }
+    "iso-8859-13": (QP, QP, None),
+    "iso-8859-14": (QP, QP, None),
+    "iso-8859-15": (QP, QP, None),
+    "iso-8859-16": (QP, QP, None),
+    "windows-1252": (QP, QP, None),
+    "viscii": (QP, QP, None),
+    "us-ascii": (None, None, None),
+    "big5": (BASE64, BASE64, None),
+    "gb2312": (BASE64, BASE64, None),
+    "euc-jp": (BASE64, None, "iso-2022-jp"),
+    "shift_jis": (BASE64, None, "iso-2022-jp"),
+    "iso-2022-jp": (BASE64, None, None),
+    "koi8-r": (BASE64, BASE64, None),
+    "utf-8": (SHORTEST, BASE64, "utf-8"),
+}
 
 # Aliases for other commonly-used names for character sets.  Map
 # them to the real ones used in email.
 ALIASES = {
-    'latin_1': 'iso-8859-1',
-    'latin-1': 'iso-8859-1',
-    'latin_2': 'iso-8859-2',
-    'latin-2': 'iso-8859-2',
-    'latin_3': 'iso-8859-3',
-    'latin-3': 'iso-8859-3',
-    'latin_4': 'iso-8859-4',
-    'latin-4': 'iso-8859-4',
-    'latin_5': 'iso-8859-9',
-    'latin-5': 'iso-8859-9',
-    'latin_6': 'iso-8859-10',
-    'latin-6': 'iso-8859-10',
-    'latin_7': 'iso-8859-13',
-    'latin-7': 'iso-8859-13',
-    'latin_8': 'iso-8859-14',
-    'latin-8': 'iso-8859-14',
-    'latin_9': 'iso-8859-15',
-    'latin-9': 'iso-8859-15',
-    'latin_10':'iso-8859-16',
-    'latin-10':'iso-8859-16',
-    'cp949':   'ks_c_5601-1987',
-    'euc_jp':  'euc-jp',
-    'euc_kr':  'euc-kr',
-    'ascii':   'us-ascii',
-    }
+    "latin_1": "iso-8859-1",
+    "latin-1": "iso-8859-1",
+    "latin_2": "iso-8859-2",
+    "latin-2": "iso-8859-2",
+    "latin_3": "iso-8859-3",
+    "latin-3": "iso-8859-3",
+    "latin_4": "iso-8859-4",
+    "latin-4": "iso-8859-4",
+    "latin_5": "iso-8859-9",
+    "latin-5": "iso-8859-9",
+    "latin_6": "iso-8859-10",
+    "latin-6": "iso-8859-10",
+    "latin_7": "iso-8859-13",
+    "latin-7": "iso-8859-13",
+    "latin_8": "iso-8859-14",
+    "latin-8": "iso-8859-14",
+    "latin_9": "iso-8859-15",
+    "latin-9": "iso-8859-15",
+    "latin_10": "iso-8859-16",
+    "latin-10": "iso-8859-16",
+    "cp949": "ks_c_5601-1987",
+    "euc_jp": "euc-jp",
+    "euc_kr": "euc-kr",
+    "ascii": "us-ascii",
+}
 
 
 # Map charsets to their Unicode codec strings.
 CODEC_MAP = {
-    'gb2312':      'eucgb2312_cn',
-    'big5':        'big5_tw',
+    "gb2312": "eucgb2312_cn",
+    "big5": "big5_tw",
     # Hack: We don't want *any* conversion for stuff marked us-ascii, as all
     # sorts of garbage might be sent to us in the guise of 7-bit us-ascii.
     # Let that stuff pass through without conversion to/from Unicode.
-    'us-ascii':    None,
-    }
+    "us-ascii": None,
+}
 
 
-
 # Convenience functions for extending the above mappings
 def add_charset(charset, header_enc=None, body_enc=None, output_charset=None):
     """Add character set properties to the global registry.
@@ -130,7 +127,7 @@ def add_charset(charset, header_enc=None, body_enc=None, output_charset=None):
     documentation for more information.
     """
     if body_enc == SHORTEST:
-        raise ValueError('SHORTEST not allowed for body_enc')
+        raise ValueError("SHORTEST not allowed for body_enc")
     CHARSETS[charset] = (header_enc, body_enc, output_charset)
 
 
@@ -153,17 +150,15 @@ def add_codec(charset, codecname):
     CODEC_MAP[charset] = codecname
 
 
-
 # Convenience function for encoding strings, taking into account
 # that they might be unknown-8bit (ie: have surrogate-escaped bytes)
 def _encode(string, codec):
     if codec == UNKNOWN8BIT:
-        return string.encode('ascii', 'surrogateescape')
+        return string.encode("ascii", "surrogateescape")
     else:
         return string.encode(codec)
 
 
-
 class Charset:
     """Map character sets to their email properties.
 
@@ -208,6 +203,7 @@ class Charset:
                   to the output_charset.  If no conversion codec is necessary,
                   this attribute will have the same value as the input_codec.
     """
+
     def __init__(self, input_charset=DEFAULT_CHARSET):
         # RFC 2046, $4.1.2 says charsets are not case sensitive.  We coerce to
         # unicode because its .lower() is locale insensitive.  If the argument
@@ -215,9 +211,9 @@ def __init__(self, input_charset=DEFAULT_CHARSET):
         # charset is ASCII, as the standard (RFC XXX) requires.
         try:
             if isinstance(input_charset, str):
-                input_charset.encode('ascii')
+                input_charset.encode("ascii")
             else:
-                input_charset = str(input_charset, 'ascii')
+                input_charset = str(input_charset, "ascii")
         except UnicodeError:
             raise errors.CharsetError(input_charset)
         input_charset = input_charset.lower()
@@ -226,8 +222,7 @@ def __init__(self, input_charset=DEFAULT_CHARSET):
         # We can try to guess which encoding and conversion to use by the
         # charset_map dictionary.  Try that first, but let the user override
         # it.
-        henc, benc, conv = CHARSETS.get(self.input_charset,
-                                        (SHORTEST, BASE64, None))
+        henc, benc, conv = CHARSETS.get(self.input_charset, (SHORTEST, BASE64, None))
         if not conv:
             conv = self.input_charset
         # Set the attributes, allowing the arguments to override the default.
@@ -236,10 +231,8 @@ def __init__(self, input_charset=DEFAULT_CHARSET):
         self.output_charset = ALIASES.get(conv, conv)
         # Now set the codecs.  If one isn't defined for input_charset,
         # guess and try a Unicode codec with the same name as input_codec.
-        self.input_codec = CODEC_MAP.get(self.input_charset,
-                                         self.input_charset)
-        self.output_codec = CODEC_MAP.get(self.output_charset,
-                                          self.output_charset)
+        self.input_codec = CODEC_MAP.get(self.input_charset, self.input_charset)
+        self.output_codec = CODEC_MAP.get(self.output_charset, self.output_charset)
 
     def __str__(self):
         return self.input_charset.lower()
@@ -267,9 +260,9 @@ def get_body_encoding(self):
         """
         assert self.body_encoding != SHORTEST
         if self.body_encoding == QP:
-            return 'quoted-printable'
+            return "quoted-printable"
         elif self.body_encoding == BASE64:
-            return 'base64'
+            return "base64"
         else:
             return encode_7or8bit
 
@@ -292,7 +285,7 @@ def header_encode(self, string):
             output codec.
         :return: The encoded string, with RFC 2047 chrome.
         """
-        codec = self.output_codec or 'us-ascii'
+        codec = self.output_codec or "us-ascii"
         header_bytes = _encode(string, codec)
         # 7bit/8bit encodings return the string unchanged (modulo conversions)
         encoder_module = self._get_encoder(header_bytes)
@@ -318,7 +311,7 @@ def header_encode_lines(self, string, maxlengths):
         :return: Lines of encoded strings, each with RFC 2047 chrome.
         """
         # See which encoding we should use.
-        codec = self.output_codec or 'us-ascii'
+        codec = self.output_codec or "us-ascii"
         header_bytes = _encode(string, codec)
         encoder_module = self._get_encoder(header_bytes)
         encoder = partial(encoder_module.header_encode, charset=codec)
@@ -351,7 +344,7 @@ def header_encode_lines(self, string, maxlengths):
                 if not lines and not current_line:
                     lines.append(None)
                 else:
-                    separator = (' ' if lines else '')
+                    separator = " " if lines else ""
                     joined_line = EMPTYSTRING.join(current_line)
                     header_bytes = _encode(joined_line, codec)
                     lines.append(encoder(header_bytes))
@@ -404,9 +397,9 @@ def body_encode(self, string):
             # being bytes has never been nailed down, so fixing that is a
             # longer term TODO.
             if isinstance(string, str):
-                string = string.encode(self.output_charset).decode('latin1')
+                string = string.encode(self.output_charset).decode("latin1")
             return email.quoprimime.body_encode(string)
         else:
             if isinstance(string, str):
-                string = string.encode(self.output_charset).decode('ascii')
+                string = string.encode(self.output_charset).decode("ascii")
             return string
diff --git a/unix-ffi/email.charset/manifest.py b/unix-ffi/email.charset/manifest.py
new file mode 100644
index 000000000..7e6dd7936
--- /dev/null
+++ b/unix-ffi/email.charset/manifest.py
@@ -0,0 +1,7 @@
+metadata(version="0.5.1")
+
+require("functools")
+require("email.encoders")
+require("email.errors")
+
+package("email")
diff --git a/email.encoders/email/base64mime.py b/unix-ffi/email.encoders/email/base64mime.py
similarity index 88%
rename from email.encoders/email/base64mime.py
rename to unix-ffi/email.encoders/email/base64mime.py
index f3bbac1ca..540c85f87 100644
--- a/email.encoders/email/base64mime.py
+++ b/unix-ffi/email.encoders/email/base64mime.py
@@ -25,27 +25,26 @@
 """
 
 __all__ = [
-    'body_decode',
-    'body_encode',
-    'decode',
-    'decodestring',
-    'header_encode',
-    'header_length',
-    ]
+    "body_decode",
+    "body_encode",
+    "decode",
+    "decodestring",
+    "header_encode",
+    "header_length",
+]
 
 
 from base64 import b64encode
 from binascii import b2a_base64, a2b_base64
 
-CRLF = '\r\n'
-NL = '\n'
-EMPTYSTRING = ''
+CRLF = "\r\n"
+NL = "\n"
+EMPTYSTRING = ""
 
 # See also Charset.py
 MISC_LEN = 7
 
 
-
 # Helpers
 def header_length(bytearray):
     """Return the length of s when it is encoded with base64."""
@@ -57,8 +56,7 @@ def header_length(bytearray):
     return n
 
 
-
-def header_encode(header_bytes, charset='iso-8859-1'):
+def header_encode(header_bytes, charset="iso-8859-1"):
     """Encode a single header line with Base64 encoding in a given charset.
 
     charset names the character set to use to encode the header.  It defaults
@@ -69,10 +67,9 @@ def header_encode(header_bytes, charset='iso-8859-1'):
     if isinstance(header_bytes, str):
         header_bytes = header_bytes.encode(charset)
     encoded = b64encode(header_bytes).decode("ascii")
-    return '=?%s?b?%s?=' % (charset, encoded)
+    return "=?%s?b?%s?=" % (charset, encoded)
 
 
-
 def body_encode(s, maxlinelen=76, eol=NL):
     r"""Encode a string with base64.
 
@@ -91,14 +88,13 @@ def body_encode(s, maxlinelen=76, eol=NL):
     for i in range(0, len(s), max_unencoded):
         # BAW: should encode() inherit b2a_base64()'s dubious behavior in
         # adding a newline to the encoded string?
-        enc = b2a_base64(s[i:i + max_unencoded]).decode("ascii")
+        enc = b2a_base64(s[i : i + max_unencoded]).decode("ascii")
         if enc.endswith(NL) and eol != NL:
             enc = enc[:-1] + eol
         encvec.append(enc)
     return EMPTYSTRING.join(encvec)
 
 
-
 def decode(string):
     """Decode a raw base64 string, returning a bytes object.
 
@@ -109,7 +105,7 @@ def decode(string):
     if not string:
         return bytes()
     elif isinstance(string, str):
-        return a2b_base64(string.encode('raw-unicode-escape'))
+        return a2b_base64(string.encode("raw-unicode-escape"))
     else:
         return a2b_base64(string)
 
diff --git a/unix-ffi/email.encoders/email/encoders.py b/unix-ffi/email.encoders/email/encoders.py
new file mode 100644
index 000000000..efa7a67ef
--- /dev/null
+++ b/unix-ffi/email.encoders/email/encoders.py
@@ -0,0 +1,74 @@
+# Copyright (C) 2001-2006 Python Software Foundation
+# Author: Barry Warsaw
+# Contact: email-sig@python.org
+
+"""Encodings and related functions."""
+
+__all__ = [
+    "encode_7or8bit",
+    "encode_base64",
+    "encode_noop",
+    "encode_quopri",
+]
+
+
+from base64 import encodebytes as _bencode
+from quopri import encodestring as _encodestring
+
+
+def _qencode(s):
+    enc = _encodestring(s, quotetabs=True)
+    # Must encode spaces, which quopri.encodestring() doesn't do
+    return enc.replace(b" ", b"=20")
+
+
+def encode_base64(msg):
+    """Encode the message's payload in Base64.
+
+    Also, add an appropriate Content-Transfer-Encoding header.
+    """
+    orig = msg.get_payload(decode=True)
+    encdata = str(_bencode(orig), "ascii")
+    msg.set_payload(encdata)
+    msg["Content-Transfer-Encoding"] = "base64"
+
+
+def encode_quopri(msg):
+    """Encode the message's payload in quoted-printable.
+
+    Also, add an appropriate Content-Transfer-Encoding header.
+    """
+    orig = msg.get_payload(decode=True)
+    encdata = _qencode(orig)
+    msg.set_payload(encdata)
+    msg["Content-Transfer-Encoding"] = "quoted-printable"
+
+
+def encode_7or8bit(msg):
+    """Set the Content-Transfer-Encoding header to 7bit or 8bit."""
+    orig = msg.get_payload(decode=True)
+    if orig is None:
+        # There's no payload.  For backwards compatibility we use 7bit
+        msg["Content-Transfer-Encoding"] = "7bit"
+        return
+    # We play a trick to make this go fast.  If encoding/decode to ASCII
+    # succeeds, we know the data must be 7bit, otherwise treat it as 8bit.
+    try:
+        if isinstance(orig, str):
+            orig.encode("ascii")
+        else:
+            orig.decode("ascii")
+    except UnicodeError:
+        charset = msg.get_charset()
+        output_cset = charset and charset.output_charset
+        # iso-2022-* is non-ASCII but encodes to a 7-bit representation
+        if output_cset and output_cset.lower().startswith("iso-2022-"):
+            msg["Content-Transfer-Encoding"] = "7bit"
+        else:
+            msg["Content-Transfer-Encoding"] = "8bit"
+    else:
+        msg["Content-Transfer-Encoding"] = "7bit"
+
+
+def encode_noop(msg):
+    """Do nothing."""
diff --git a/email.encoders/email/quoprimime.py b/unix-ffi/email.encoders/email/quoprimime.py
similarity index 88%
rename from email.encoders/email/quoprimime.py
rename to unix-ffi/email.encoders/email/quoprimime.py
index bc02281b1..d62b53287 100644
--- a/email.encoders/email/quoprimime.py
+++ b/unix-ffi/email.encoders/email/quoprimime.py
@@ -27,49 +27,50 @@
 """
 
 __all__ = [
-    'body_decode',
-    'body_encode',
-    'body_length',
-    'decode',
-    'decodestring',
-    'header_decode',
-    'header_encode',
-    'header_length',
-    'quote',
-    'unquote',
-    ]
+    "body_decode",
+    "body_encode",
+    "body_length",
+    "decode",
+    "decodestring",
+    "header_decode",
+    "header_encode",
+    "header_length",
+    "quote",
+    "unquote",
+]
 
 import re
 import io
 
 from string import ascii_letters, digits, hexdigits
 
-CRLF = '\r\n'
-NL = '\n'
-EMPTYSTRING = ''
+CRLF = "\r\n"
+NL = "\n"
+EMPTYSTRING = ""
 
 # Build a mapping of octets to the expansion of that octet.  Since we're only
 # going to have 256 of these things, this isn't terribly inefficient
 # space-wise.  Remember that headers and bodies have different sets of safe
 # characters.  Initialize both maps with the full expansion, and then override
 # the safe bytes with the more compact form.
-_QUOPRI_HEADER_MAP = dict((c, '=%02X' % c) for c in range(256))
+_QUOPRI_HEADER_MAP = dict((c, "=%02X" % c) for c in range(256))
 _QUOPRI_BODY_MAP = _QUOPRI_HEADER_MAP.copy()
 
 # Safe header bytes which need no encoding.
-for c in b'-!*+/' + ascii_letters.encode('ascii') + digits.encode('ascii'):
+for c in b"-!*+/" + ascii_letters.encode("ascii") + digits.encode("ascii"):
     _QUOPRI_HEADER_MAP[c] = chr(c)
 # Headers have one other special encoding; spaces become underscores.
-_QUOPRI_HEADER_MAP[ord(' ')] = '_'
+_QUOPRI_HEADER_MAP[ord(" ")] = "_"
 
 # Safe body bytes which need no encoding.
-for c in (b' !"#$%&\'()*+,-./0123456789:;<>'
-          b'?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`'
-          b'abcdefghijklmnopqrstuvwxyz{|}~\t'):
+for c in (
+    b" !\"#$%&'()*+,-./0123456789:;<>"
+    b"?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"
+    b"abcdefghijklmnopqrstuvwxyz{|}~\t"
+):
     _QUOPRI_BODY_MAP[c] = chr(c)
 
 
-
 # Helpers
 def header_check(octet):
     """Return True if the octet should be escaped with header quopri."""
@@ -104,7 +105,7 @@ def body_length(bytearray):
     return sum(len(_QUOPRI_BODY_MAP[octet]) for octet in bytearray)
 
 
-def _max_append(L, s, maxlen, extra=''):
+def _max_append(L, s, maxlen, extra=""):
     if not isinstance(s, str):
         s = chr(s)
     if not L:
@@ -121,11 +122,10 @@ def unquote(s):
 
 
 def quote(c):
-    return '=%02X' % ord(c)
-
+    return "=%02X" % ord(c)
 
 
-def header_encode(header_bytes, charset='iso-8859-1'):
+def header_encode(header_bytes, charset="iso-8859-1"):
     """Encode a single header line with quoted-printable (like) encoding.
 
     Defined in RFC 2045, this `Q' encoding is similar to quoted-printable, but
@@ -138,18 +138,17 @@ def header_encode(header_bytes, charset='iso-8859-1'):
     """
     # Return empty headers as an empty string.
     if not header_bytes:
-        return ''
+        return ""
     # Iterate over every byte, encoding if necessary.
     encoded = []
     for octet in header_bytes:
         encoded.append(_QUOPRI_HEADER_MAP[octet])
     # Now add the RFC chrome to each encoded chunk and glue the chunks
     # together.
-    return '=?%s?q?%s?=' % (charset, EMPTYSTRING.join(encoded))
+    return "=?%s?q?%s?=" % (charset, EMPTYSTRING.join(encoded))
 
 
 class _body_accumulator(io.StringIO):
-
     def __init__(self, maxlinelen, eol, *args, **kw):
         super().__init__(*args, **kw)
         self.eol = eol
@@ -167,7 +166,7 @@ def newline(self):
 
     def write_soft_break(self):
         """Write a soft break, then start a new line."""
-        self.write_str('=')
+        self.write_str("=")
         self.newline()
 
     def write_wrapped(self, s, extra_room=0):
@@ -182,7 +181,7 @@ def write_char(self, c, is_last_char):
             # extra room, either for it or a soft break, and whitespace
             # need not be quoted.
             self.write_wrapped(c, extra_room=1)
-        elif c not in ' \t':
+        elif c not in " \t":
             # For this and remaining cases, no more characters follow,
             # so there is no need to reserve extra room (since a hard
             # break will immediately follow).
@@ -227,7 +226,7 @@ def body_encode(body, maxlinelen=76, eol=NL):
         return body
 
     # The last line may or may not end in eol, but all other lines do.
-    last_has_eol = (body[-1] in '\r\n')
+    last_has_eol = body[-1] in "\r\n"
 
     # This accumulator will make it easier to build the encoded body.
     encoded_body = _body_accumulator(maxlinelen, eol)
@@ -239,7 +238,7 @@ def body_encode(body, maxlinelen=76, eol=NL):
         for i, c in enumerate(line):
             if body_check(ord(c)):
                 c = quote(c)
-            encoded_body.write_char(c, i==last_char_index)
+            encoded_body.write_char(c, i == last_char_index)
         # Add an eol if input line had eol.  All input lines have eol except
         # possibly the last one.
         if line_no < last_line_no or last_has_eol:
@@ -248,7 +247,6 @@ def body_encode(body, maxlinelen=76, eol=NL):
     return encoded_body.getvalue()
 
 
-
 # BAW: I'm not sure if the intent was for the signature of this function to be
 # the same as base64MIME.decode() or not...
 def decode(encoded, eol=NL):
@@ -261,7 +259,7 @@ def decode(encoded, eol=NL):
     # BAW: see comment in encode() above.  Again, we're building up the
     # decoded string with string concatenation, which could be done much more
     # efficiently.
-    decoded = ''
+    decoded = ""
 
     for line in encoded.splitlines():
         line = line.rstrip()
@@ -273,17 +271,17 @@ def decode(encoded, eol=NL):
         n = len(line)
         while i < n:
             c = line[i]
-            if c != '=':
+            if c != "=":
                 decoded += c
                 i += 1
             # Otherwise, c == "=".  Are we at the end of the line?  If so, add
             # a soft line break.
-            elif i+1 == n:
+            elif i + 1 == n:
                 i += 1
                 continue
             # Decode if in form =AB
-            elif i+2 < n and line[i+1] in hexdigits and line[i+2] in hexdigits:
-                decoded += unquote(line[i:i+3])
+            elif i + 2 < n and line[i + 1] in hexdigits and line[i + 2] in hexdigits:
+                decoded += unquote(line[i : i + 3])
                 i += 3
             # Otherwise, not in form =AB, pass literally
             else:
@@ -293,7 +291,7 @@ def decode(encoded, eol=NL):
             if i == n:
                 decoded += eol
     # Special case if original string did not end with eol
-    if encoded[-1] not in '\r\n' and decoded.endswith(eol):
+    if encoded[-1] not in "\r\n" and decoded.endswith(eol):
         decoded = decoded[:-1]
     return decoded
 
@@ -303,7 +301,6 @@ def decode(encoded, eol=NL):
 decodestring = decode
 
 
-
 def _unquote_match(match):
     """Turn a match in the form =AB to the ASCII character with value 0xab"""
     s = match.group(0)
@@ -318,5 +315,5 @@ def header_decode(s):
     quoted-printable (like =?iso-8895-1?q?Hello_World?=) -- please use
     the high level email.header class for that functionality.
     """
-    s = s.replace('_', ' ')
-    return re.sub(r'=[a-fA-F0-9]{2}', _unquote_match, s, flags=re.ASCII)
+    s = s.replace("_", " ")
+    return re.sub(r"=[a-fA-F0-9]{2}", _unquote_match, s, flags=re.ASCII)
diff --git a/unix-ffi/email.encoders/manifest.py b/unix-ffi/email.encoders/manifest.py
new file mode 100644
index 000000000..a3e735d8c
--- /dev/null
+++ b/unix-ffi/email.encoders/manifest.py
@@ -0,0 +1,9 @@
+metadata(version="0.5.1")
+
+require("base64")
+require("binascii")
+require("quopri")
+require("re")
+require("string")
+
+package("email")
diff --git a/email.errors/email/errors.py b/unix-ffi/email.errors/email/errors.py
similarity index 94%
rename from email.errors/email/errors.py
rename to unix-ffi/email.errors/email/errors.py
index d5444f6e0..4c76f6f7d 100644
--- a/email.errors/email/errors.py
+++ b/unix-ffi/email.errors/email/errors.py
@@ -21,7 +21,7 @@ class BoundaryError(MessageParseError):
     """Couldn't find terminating boundary."""
 
 
-class MultipartConversionError(MessageError):#, TypeError):
+class MultipartConversionError(MessageError):  # , TypeError):
     """Conversion to a multipart is prohibited."""
 
 
@@ -38,55 +38,73 @@ def __init__(self, line=None):
             super().__init__(line)
         self.line = line
 
+
 class NoBoundaryInMultipartDefect(MessageDefect):
     """A message claimed to be a multipart but had no boundary parameter."""
 
+
 class StartBoundaryNotFoundDefect(MessageDefect):
     """The claimed start boundary was never found."""
 
+
 class CloseBoundaryNotFoundDefect(MessageDefect):
     """A start boundary was found, but not the corresponding close boundary."""
 
+
 class FirstHeaderLineIsContinuationDefect(MessageDefect):
     """A message had a continuation line as its first header line."""
 
+
 class MisplacedEnvelopeHeaderDefect(MessageDefect):
     """A 'Unix-from' header was found in the middle of a header block."""
 
+
 class MissingHeaderBodySeparatorDefect(MessageDefect):
     """Found line with no leading whitespace and no colon before blank line."""
+
+
 # XXX: backward compatibility, just in case (it was never emitted).
 MalformedHeaderDefect = MissingHeaderBodySeparatorDefect
 
+
 class MultipartInvariantViolationDefect(MessageDefect):
     """A message claimed to be a multipart but no subparts were found."""
 
+
 class InvalidMultipartContentTransferEncodingDefect(MessageDefect):
     """An invalid content transfer encoding was set on the multipart itself."""
 
+
 class UndecodableBytesDefect(MessageDefect):
     """Header contained bytes that could not be decoded"""
 
+
 class InvalidBase64PaddingDefect(MessageDefect):
     """base64 encoded sequence had an incorrect length"""
 
+
 class InvalidBase64CharactersDefect(MessageDefect):
     """base64 encoded sequence had characters not in base64 alphabet"""
 
+
 # These errors are specific to header parsing.
 
+
 class HeaderDefect(MessageDefect):
     """Base class for a header defect."""
 
     def __init__(self, *args, **kw):
         super().__init__(*args, **kw)
 
+
 class InvalidHeaderDefect(HeaderDefect):
     """Header is not valid, message gives details."""
 
+
 class HeaderMissingRequiredValue(HeaderDefect):
     """A header that must have a value had none"""
 
+
 class NonPrintableDefect(HeaderDefect):
     """ASCII characters outside the ascii-printable range found"""
 
@@ -95,13 +113,17 @@ def __init__(self, non_printables):
         self.non_printables = non_printables
 
     def __str__(self):
-        return ("the following ASCII non-printables found in header: "
-            "{}".format(self.non_printables))
+        return "the following ASCII non-printables found in header: " "{}".format(
+            self.non_printables
+        )
+
 
 class ObsoleteHeaderDefect(HeaderDefect):
     """Header uses syntax declared obsolete by RFC 5322"""
 
+
 class NonASCIILocalPartDefect(HeaderDefect):
     """local_part contains non-ASCII characters"""
+
     # This defect only occurs during unicode parsing, not when
     # parsing messages decoded from binary.
diff --git a/unix-ffi/email.errors/manifest.py b/unix-ffi/email.errors/manifest.py
new file mode 100644
index 000000000..6a2e0be77
--- /dev/null
+++ b/unix-ffi/email.errors/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.5.1")
+
+package("email")
diff --git a/email.feedparser/email/feedparser.py b/unix-ffi/email.feedparser/email/feedparser.py
similarity index 88%
rename from email.feedparser/email/feedparser.py
rename to unix-ffi/email.feedparser/email/feedparser.py
index 12b64ce19..15f7fffa7 100644
--- a/email.feedparser/email/feedparser.py
+++ b/unix-ffi/email.feedparser/email/feedparser.py
@@ -19,7 +19,7 @@
 object's .defects attribute.
 """
 
-__all__ = ['FeedParser', 'BytesFeedParser']
+__all__ = ["FeedParser", "BytesFeedParser"]
 
 import re
 
@@ -27,20 +27,19 @@
 from email import message
 from email._policybase import compat32
 
-NLCRE = re.compile('\r\n|\r|\n')
-NLCRE_bol = re.compile('(\r\n|\r|\n)')
-NLCRE_eol = re.compile('(\r\n|\r|\n)\Z')
-NLCRE_crack = re.compile('(\r\n|\r|\n)')
+NLCRE = re.compile("\r\n|\r|\n")
+NLCRE_bol = re.compile("(\r\n|\r|\n)")
+NLCRE_eol = re.compile("(\r\n|\r|\n)\Z")
+NLCRE_crack = re.compile("(\r\n|\r|\n)")
 # RFC 2822 $3.6.8 Optional fields.  ftext is %d33-57 / %d59-126, Any character
 # except controls, SP, and ":".
-headerRE = re.compile(r'^(From |[\041-\071\073-\176]{1,}:|[\t ])')
-EMPTYSTRING = ''
-NL = '\n'
+headerRE = re.compile(r"^(From |[\041-\071\073-\176]{1,}:|[\t ])")
+EMPTYSTRING = ""
+NL = "\n"
 
 NeedMoreData = object()
 
 
-
 class BufferedSubFile(object):
     """A file-ish object that can have new data loaded into it.
 
@@ -49,9 +48,10 @@ class BufferedSubFile(object):
     (i.e. empty string) is returned instead.  This lets the parser adhere to a
     simple abstraction -- it parses until EOF closes the current message.
     """
+
     def __init__(self):
         # The last partial line pushed into this object.
-        self._partial = ''
+        self._partial = ""
         # The list of full, pushed lines, in reverse order
         self._lines = []
         # The stack of false-EOF checking predicates.
@@ -68,13 +68,13 @@ def pop_eof_matcher(self):
     def close(self):
         # Don't forget any trailing partial line.
         self._lines.append(self._partial)
-        self._partial = ''
+        self._partial = ""
         self._closed = True
 
     def readline(self):
         if not self._lines:
             if self._closed:
-                return ''
+                return ""
             return NeedMoreData
         # Pop the line off the stack and see if it matches the current
         # false-EOF predicate.
@@ -86,7 +86,7 @@ def readline(self):
             if ateof(line):
                 # We're at the false EOF.  But push the last line back first.
                 self._lines.append(line)
-                return ''
+                return ""
         return line
 
     def unreadline(self, line):
@@ -97,7 +97,7 @@ def unreadline(self, line):
     def push(self, data):
         """Push some new data into this object."""
         # Handle any previous leftovers
-        data, self._partial = self._partial + data, ''
+        data, self._partial = self._partial + data, ""
         # Crack into lines, but preserve the newlines on the end of each
         parts = NLCRE_crack.split(data)
         # The *ahem* interesting behaviour of re.split when supplied grouping
@@ -105,16 +105,16 @@ def push(self, data):
         # data after the final RE.  In the case of a NL/CR terminated string,
         # this is the empty string.
         self._partial = parts.pop()
-        #GAN 29Mar09  bugs 1555570, 1721862  Confusion at 8K boundary ending with \r:
+        # GAN 29Mar09  bugs 1555570, 1721862  Confusion at 8K boundary ending with \r:
         # is there a \n to follow later?
-        if not self._partial and parts and parts[-1].endswith('\r'):
-            self._partial = parts.pop(-2)+parts.pop()
+        if not self._partial and parts and parts[-1].endswith("\r"):
+            self._partial = parts.pop(-2) + parts.pop()
         # parts is a list of strings, alternating between the line contents
         # and the eol character(s).  Gather up a list of lines after
         # re-attaching the newlines.
         lines = []
         for i in range(len(parts) // 2):
-            lines.append(parts[i*2] + parts[i*2+1])
+            lines.append(parts[i * 2] + parts[i * 2 + 1])
         self.pushlines(lines)
 
     def pushlines(self, lines):
@@ -126,12 +126,11 @@ def __iter__(self):
 
     def __next__(self):
         line = self.readline()
-        if line == '':
+        if line == "":
             raise StopIteration
         return line
 
 
-
 class FeedParser:
     """A feed-style parser of email."""
 
@@ -147,7 +146,7 @@ def __init__(self, _factory=message.Message, policy=compat32):
         self.policy = policy
         try:
             _factory(policy=self.policy)
-            self._factory_kwds = lambda: {'policy': self.policy}
+            self._factory_kwds = lambda: {"policy": self.policy}
         except TypeError:
             # Assume this is an old-style factory
             self._factory_kwds = lambda: {}
@@ -180,16 +179,15 @@ def close(self):
         root = self._pop_message()
         assert not self._msgstack
         # Look for final set of defects
-        if root.get_content_maintype() == 'multipart' \
-               and not root.is_multipart():
+        if root.get_content_maintype() == "multipart" and not root.is_multipart():
             defect = errors.MultipartInvariantViolationDefect()
             self.policy.handle_defect(root, defect)
         return root
 
     def _new_message(self):
         msg = self._factory(**self._factory_kwds())
-        if self._cur and self._cur.get_content_type() == 'multipart/digest':
-            msg.set_default_type('message/rfc822')
+        if self._cur and self._cur.get_content_type() == "multipart/digest":
+            msg.set_default_type("message/rfc822")
         if self._msgstack:
             self._msgstack[-1].attach(msg)
         self._msgstack.append(msg)
@@ -237,12 +235,12 @@ def _parsegen(self):
                 if line is NeedMoreData:
                     yield NeedMoreData
                     continue
-                if line == '':
+                if line == "":
                     break
                 lines.append(line)
             self._cur.set_payload(EMPTYSTRING.join(lines))
             return
-        if self._cur.get_content_type() == 'message/delivery-status':
+        if self._cur.get_content_type() == "message/delivery-status":
             # message/delivery-status contains blocks of headers separated by
             # a blank line.  We'll represent each header block as a separate
             # nested message object, but the processing is a bit different
@@ -276,12 +274,12 @@ def _parsegen(self):
                         yield NeedMoreData
                         continue
                     break
-                if line == '':
+                if line == "":
                     break
                 # Not at EOF so this is a line we're going to need.
                 self._input.unreadline(line)
             return
-        if self._cur.get_content_maintype() == 'message':
+        if self._cur.get_content_maintype() == "message":
             # The message claims to be a message/* type, then what follows is
             # another RFC 2822 message.
             for retval in self._parsegen():
@@ -291,7 +289,7 @@ def _parsegen(self):
                 break
             self._pop_message()
             return
-        if self._cur.get_content_maintype() == 'multipart':
+        if self._cur.get_content_maintype() == "multipart":
             boundary = self._cur.get_boundary()
             if boundary is None:
                 # The message /claims/ to be a multipart but it has not
@@ -309,18 +307,23 @@ def _parsegen(self):
                 self._cur.set_payload(EMPTYSTRING.join(lines))
                 return
             # Make sure a valid content type was specified per RFC 2045:6.4.
-            if (self._cur.get('content-transfer-encoding', '8bit').lower()
-                    not in ('7bit', '8bit', 'binary')):
+            if self._cur.get("content-transfer-encoding", "8bit").lower() not in (
+                "7bit",
+                "8bit",
+                "binary",
+            ):
                 defect = errors.InvalidMultipartContentTransferEncodingDefect()
                 self.policy.handle_defect(self._cur, defect)
             # Create a line match predicate which matches the inter-part
             # boundary as well as the end-of-multipart boundary.  Don't push
             # this onto the input stream until we've scanned past the
             # preamble.
-            separator = '--' + boundary
+            separator = "--" + boundary
             boundaryre = re.compile(
-                '(?P<sep>' + re.escape(separator) +
-                r')(?P<end>--)?(?P<ws>[ \t]*)(?P<linesep>\r\n|\r|\n)?$')
+                "(?P<sep>"
+                + re.escape(separator)
+                + r")(?P<end>--)?(?P<ws>[ \t]*)(?P<linesep>\r\n|\r|\n)?$"
+            )
             capturing_preamble = True
             preamble = []
             linesep = False
@@ -330,7 +333,7 @@ def _parsegen(self):
                 if line is NeedMoreData:
                     yield NeedMoreData
                     continue
-                if line == '':
+                if line == "":
                     break
                 mo = boundaryre.match(line)
                 if mo:
@@ -338,9 +341,9 @@ def _parsegen(self):
                     # this multipart.  If there was a newline at the end of
                     # the closing boundary, then we need to initialize the
                     # epilogue with the empty string (see below).
-                    if mo.group('end'):
+                    if mo.group("end"):
                         close_boundary_seen = True
-                        linesep = mo.group('linesep')
+                        linesep = mo.group("linesep")
                         break
                     # We saw an inter-part boundary.  Were we in the preamble?
                     if capturing_preamble:
@@ -350,7 +353,7 @@ def _parsegen(self):
                             lastline = preamble[-1]
                             eolmo = NLCRE_eol.search(lastline)
                             if eolmo:
-                                preamble[-1] = lastline[:-len(eolmo.group(0))]
+                                preamble[-1] = lastline[: -len(eolmo.group(0))]
                             self._cur.preamble = EMPTYSTRING.join(preamble)
                         capturing_preamble = False
                         self._input.unreadline(line)
@@ -380,9 +383,9 @@ def _parsegen(self):
                     # separator actually belongs to the boundary, not the
                     # previous subpart's payload (or epilogue if the previous
                     # part is a multipart).
-                    if self._last.get_content_maintype() == 'multipart':
+                    if self._last.get_content_maintype() == "multipart":
                         epilogue = self._last.epilogue
-                        if epilogue == '':
+                        if epilogue == "":
                             self._last.epilogue = None
                         elif epilogue is not None:
                             mo = NLCRE_eol.search(epilogue)
@@ -394,7 +397,7 @@ def _parsegen(self):
                         if isinstance(payload, str):
                             mo = NLCRE_eol.search(payload)
                             if mo:
-                                payload = payload[:-len(mo.group(0))]
+                                payload = payload[: -len(mo.group(0))]
                                 self._last._payload = payload
                     self._input.pop_eof_matcher()
                     self._pop_message()
@@ -429,7 +432,7 @@ def _parsegen(self):
             # ended in a newline, we'll need to make sure the epilogue isn't
             # None
             if linesep:
-                epilogue = ['']
+                epilogue = [""]
             else:
                 epilogue = []
             for line in self._input:
@@ -444,7 +447,7 @@ def _parsegen(self):
                 firstline = epilogue[0]
                 bolmo = NLCRE_bol.match(firstline)
                 if bolmo:
-                    epilogue[0] = firstline[len(bolmo.group(0)):]
+                    epilogue[0] = firstline[len(bolmo.group(0)) :]
             self._cur.epilogue = EMPTYSTRING.join(epilogue)
             return
         # Otherwise, it's some non-multipart type, so the entire rest of the
@@ -459,11 +462,11 @@ def _parsegen(self):
 
     def _parse_headers(self, lines):
         # Passed a list of lines that make up the headers for the current msg
-        lastheader = ''
+        lastheader = ""
         lastvalue = []
         for lineno, line in enumerate(lines):
             # Check for continuation
-            if line[0] in ' \t':
+            if line[0] in " \t":
                 if not lastheader:
                     # The first line of the headers was a continuation.  This
                     # is illegal, so let's note the defect, store the illegal
@@ -475,14 +478,14 @@ def _parse_headers(self, lines):
                 continue
             if lastheader:
                 self._cur.set_raw(*self.policy.header_source_parse(lastvalue))
-                lastheader, lastvalue = '', []
+                lastheader, lastvalue = "", []
             # Check for envelope header, i.e. unix-from
-            if line.startswith('From '):
+            if line.startswith("From "):
                 if lineno == 0:
                     # Strip off the trailing newline
                     mo = NLCRE_eol.search(line)
                     if mo:
-                        line = line[:-len(mo.group(0))]
+                        line = line[: -len(mo.group(0))]
                     self._cur.set_unixfrom(line)
                     continue
                 elif lineno == len(lines) - 1:
@@ -500,8 +503,8 @@ def _parse_headers(self, lines):
             # Split the line on the colon separating field name from value.
             # There will always be a colon, because if there wasn't the part of
             # the parser that calls us would have started parsing the body.
-            i = line.find(':')
-            assert i>0, "_parse_headers fed line with no : and no leading WS"
+            i = line.find(":")
+            assert i > 0, "_parse_headers fed line with no : and no leading WS"
             lastheader = line[:i]
             lastvalue = [line]
         # Done with all the lines, so handle the last header.
@@ -513,4 +516,4 @@ class BytesFeedParser(FeedParser):
     """Like FeedParser, but feed accepts bytes."""
 
     def feed(self, data):
-        super().feed(data.decode('ascii', 'surrogateescape'))
+        super().feed(data.decode("ascii", "surrogateescape"))
diff --git a/unix-ffi/email.feedparser/manifest.py b/unix-ffi/email.feedparser/manifest.py
new file mode 100644
index 000000000..31c34ba91
--- /dev/null
+++ b/unix-ffi/email.feedparser/manifest.py
@@ -0,0 +1,8 @@
+metadata(version="0.5.1")
+
+require("re")
+require("email.errors")
+require("email.message")
+require("email.internal")
+
+package("email")
diff --git a/email.header/email/header.py b/unix-ffi/email.header/email/header.py
similarity index 84%
rename from email.header/email/header.py
rename to unix-ffi/email.header/email/header.py
index 42bcc0c1a..7c754112c 100644
--- a/email.header/email/header.py
+++ b/unix-ffi/email.header/email/header.py
@@ -5,10 +5,10 @@
 """Header encoding and decoding functionality."""
 
 __all__ = [
-    'Header',
-    'decode_header',
-    'make_header',
-    ]
+    "Header",
+    "decode_header",
+    "make_header",
+]
 
 import re
 import binascii
@@ -18,21 +18,23 @@
 
 from email.errors import HeaderParseError
 from email import charset as _charset
+
 Charset = _charset.Charset
 
-NL = '\n'
-SPACE = ' '
-BSPACE = b' '
-SPACE8 = ' ' * 8
-EMPTYSTRING = ''
+NL = "\n"
+SPACE = " "
+BSPACE = b" "
+SPACE8 = " " * 8
+EMPTYSTRING = ""
 MAXLINELEN = 78
-FWS = ' \t'
+FWS = " \t"
 
-USASCII = Charset('us-ascii')
-UTF8 = Charset('utf-8')
+USASCII = Charset("us-ascii")
+UTF8 = Charset("utf-8")
 
 # Match encoded-word strings in the form =?charset?q?Hello_World?=
-ecre = re.compile(r'''
+ecre = re.compile(
+    r"""
   =\?                   # literal =?
   (?P<charset>[^?]*?)   # non-greedy up to the next ? is the charset
   \?                    # literal ?
@@ -40,24 +42,24 @@
   \?                    # literal ?
   (?P<encoded>.*?)      # non-greedy up to the next ?= is the encoded string
   \?=                   # literal ?=
-  ''', re.VERBOSE | re.IGNORECASE | re.MULTILINE)
+  """,
+    re.VERBOSE | re.IGNORECASE | re.MULTILINE,
+)
 
 # Field name regexp, including trailing colon, but not separating whitespace,
 # according to RFC 2822.  Character range is from tilde to exclamation mark.
 # For use with .match()
-fcre = re.compile(r'[\041-\176]+:$')
+fcre = re.compile(r"[\041-\176]+:$")
 
 # Find a header embedded in a putative header value.  Used to check for
 # header injection attack.
-_embeded_header = re.compile(r'\n[^ \t]+:')
+_embeded_header = re.compile(r"\n[^ \t]+:")
 
 
-
 # Helpers
 _max_append = email.quoprimime._max_append
 
 
-
 def decode_header(header):
     """Decode a message header value without converting charset.
 
@@ -73,9 +75,11 @@ def decode_header(header):
     occurs (e.g. a base64 decoding exception).
     """
     # If it is a Header object, we can just return the encoded chunks.
-    if hasattr(header, '_chunks'):
-        return [(_charset._encode(string, str(charset)), str(charset))
-                    for string, charset in header._chunks]
+    if hasattr(header, "_chunks"):
+        return [
+            (_charset._encode(string, str(charset)), str(charset))
+            for string, charset in header._chunks
+        ]
     # If no encoding, just return the header with no charset.
     if not ecre.search(header):
         return [(header, None)]
@@ -101,10 +105,11 @@ def decode_header(header):
     # Now loop over words and remove words that consist of whitespace
     # between two encoded strings.
     import sys
+
     droplist = []
     for n, w in enumerate(words):
-        if n>1 and w[1] and words[n-2][1] and words[n-1][0].isspace():
-            droplist.append(n-1)
+        if n > 1 and w[1] and words[n - 2][1] and words[n - 1][0].isspace():
+            droplist.append(n - 1)
     for d in reversed(droplist):
         del words[d]
 
@@ -116,28 +121,28 @@ def decode_header(header):
         if encoding is None:
             # This is an unencoded word.
             decoded_words.append((encoded_string, charset))
-        elif encoding == 'q':
+        elif encoding == "q":
             word = email.quoprimime.header_decode(encoded_string)
             decoded_words.append((word, charset))
-        elif encoding == 'b':
-            paderr = len(encoded_string) % 4   # Postel's law: add missing padding
+        elif encoding == "b":
+            paderr = len(encoded_string) % 4  # Postel's law: add missing padding
             if paderr:
-                encoded_string += '==='[:4 - paderr]
+                encoded_string += "==="[: 4 - paderr]
             try:
                 word = email.base64mime.decode(encoded_string)
             except binascii.Error:
-                raise HeaderParseError('Base64 decoding error')
+                raise HeaderParseError("Base64 decoding error")
             else:
                 decoded_words.append((word, charset))
         else:
-            raise AssertionError('Unexpected encoding: ' + encoding)
+            raise AssertionError("Unexpected encoding: " + encoding)
     # Now convert all words to bytes and collapse consecutive runs of
     # similarly encoded words.
     collapsed = []
     last_word = last_charset = None
     for word, charset in decoded_words:
         if isinstance(word, str):
-            word = bytes(word, 'raw-unicode-escape')
+            word = bytes(word, "raw-unicode-escape")
         if last_word is None:
             last_word = word
             last_charset = charset
@@ -153,9 +158,7 @@ def decode_header(header):
     return collapsed
 
 
-
-def make_header(decoded_seq, maxlinelen=None, header_name=None,
-                continuation_ws=' '):
+def make_header(decoded_seq, maxlinelen=None, header_name=None, continuation_ws=" "):
     """Create a Header from a sequence of pairs as returned by decode_header()
 
     decode_header() takes a header value string and returns a sequence of
@@ -166,8 +169,7 @@ def make_header(decoded_seq, maxlinelen=None, header_name=None,
     instance.  Optional maxlinelen, header_name, and continuation_ws are as in
     the Header constructor.
     """
-    h = Header(maxlinelen=maxlinelen, header_name=header_name,
-               continuation_ws=continuation_ws)
+    h = Header(maxlinelen=maxlinelen, header_name=header_name, continuation_ws=continuation_ws)
     for s, charset in decoded_seq:
         # None means us-ascii but we can simply pass it on to h.append()
         if charset is not None and not isinstance(charset, Charset):
@@ -176,11 +178,16 @@ def make_header(decoded_seq, maxlinelen=None, header_name=None,
     return h
 
 
-
 class Header:
-    def __init__(self, s=None, charset=None,
-                 maxlinelen=None, header_name=None,
-                 continuation_ws=' ', errors='strict'):
+    def __init__(
+        self,
+        s=None,
+        charset=None,
+        maxlinelen=None,
+        header_name=None,
+        continuation_ws=" ",
+        errors="strict",
+    ):
         """Create a MIME-compliant header that can contain many character sets.
 
         Optional s is the initial header value.  If None, the initial header
@@ -240,15 +247,15 @@ def __str__(self):
             # a space (trailing or leading depending on transition)
             nextcs = charset
             if nextcs == _charset.UNKNOWN8BIT:
-                original_bytes = string.encode('ascii', 'surrogateescape')
-                string = original_bytes.decode('ascii', 'replace')
+                original_bytes = string.encode("ascii", "surrogateescape")
+                string = original_bytes.decode("ascii", "replace")
             if uchunks:
                 hasspace = string and self._nonctext(string[0])
-                if lastcs not in (None, 'us-ascii'):
-                    if nextcs in (None, 'us-ascii') and not hasspace:
+                if lastcs not in (None, "us-ascii"):
+                    if nextcs in (None, "us-ascii") and not hasspace:
                         uchunks.append(SPACE)
                         nextcs = None
-                elif nextcs not in (None, 'us-ascii') and not lastspace:
+                elif nextcs not in (None, "us-ascii") and not lastspace:
                     uchunks.append(SPACE)
             lastspace = string and self._nonctext(string[-1])
             lastcs = nextcs
@@ -266,7 +273,7 @@ def __eq__(self, other):
     def __ne__(self, other):
         return not self == other
 
-    def append(self, s, charset=None, errors='strict'):
+    def append(self, s, charset=None, errors="strict"):
         """Append a string to the MIME header.
 
         Optional charset, if given, should be a Charset instance or the name
@@ -292,29 +299,28 @@ def append(self, s, charset=None, errors='strict'):
         elif not isinstance(charset, Charset):
             charset = Charset(charset)
         if not isinstance(s, str):
-            input_charset = charset.input_codec or 'us-ascii'
+            input_charset = charset.input_codec or "us-ascii"
             if input_charset == _charset.UNKNOWN8BIT:
-                s = s.decode('us-ascii', 'surrogateescape')
+                s = s.decode("us-ascii", "surrogateescape")
             else:
                 s = s.decode(input_charset, errors)
         # Ensure that the bytes we're storing can be decoded to the output
         # character set, otherwise an early error is raised.
-        output_charset = charset.output_codec or 'us-ascii'
+        output_charset = charset.output_codec or "us-ascii"
         if output_charset != _charset.UNKNOWN8BIT:
             try:
                 s.encode(output_charset, errors)
             except UnicodeEncodeError:
-                if output_charset!='us-ascii':
+                if output_charset != "us-ascii":
                     raise
                 charset = UTF8
         self._chunks.append((s, charset))
 
     def _nonctext(self, s):
-        """True if string s is not a ctext character of RFC822.
-        """
-        return s in (' ', '\t', '(', ')', '\\')
+        """True if string s is not a ctext character of RFC822."""
+        return s in (" ", "\t", "(", ")", "\\")
 
-    def encode(self, splitchars=';, \t', maxlinelen=None, linesep='\n'):
+    def encode(self, splitchars=";, \t", maxlinelen=None, linesep="\n"):
         r"""Encode a message header into an RFC-compliant format.
 
         There are many issues involved in converting a given string for use in
@@ -355,35 +361,34 @@ def encode(self, splitchars=';, \t', maxlinelen=None, linesep='\n'):
         # _ValueFormatter algorithm much simpler.
         if maxlinelen == 0:
             maxlinelen = 1000000
-        formatter = _ValueFormatter(self._headerlen, maxlinelen,
-                                    self._continuation_ws, splitchars)
+        formatter = _ValueFormatter(self._headerlen, maxlinelen, self._continuation_ws, splitchars)
         lastcs = None
         hasspace = lastspace = None
         for string, charset in self._chunks:
             if hasspace is not None:
                 hasspace = string and self._nonctext(string[0])
                 import sys
-                if lastcs not in (None, 'us-ascii'):
-                    if not hasspace or charset not in (None, 'us-ascii'):
+
+                if lastcs not in (None, "us-ascii"):
+                    if not hasspace or charset not in (None, "us-ascii"):
                         formatter.add_transition()
-                elif charset not in (None, 'us-ascii') and not lastspace:
+                elif charset not in (None, "us-ascii") and not lastspace:
                     formatter.add_transition()
             lastspace = string and self._nonctext(string[-1])
             lastcs = charset
             hasspace = False
             lines = string.splitlines()
             if lines:
-                formatter.feed('', lines[0], charset)
+                formatter.feed("", lines[0], charset)
             else:
-                formatter.feed('', '', charset)
+                formatter.feed("", "", charset)
             for line in lines[1:]:
                 formatter.newline()
                 if charset.header_encoding is not None:
-                    formatter.feed(self._continuation_ws, ' ' + line.lstrip(),
-                                   charset)
+                    formatter.feed(self._continuation_ws, " " + line.lstrip(), charset)
                 else:
                     sline = line.lstrip()
-                    fws = line[:len(line)-len(sline)]
+                    fws = line[: len(line) - len(sline)]
                     formatter.feed(fws, sline, charset)
             if len(lines) > 1:
                 formatter.newline()
@@ -391,8 +396,9 @@ def encode(self, splitchars=';, \t', maxlinelen=None, linesep='\n'):
             formatter.add_transition()
         value = formatter._str(linesep)
         if _embeded_header.search(value):
-            raise HeaderParseError("header value appears to contain "
-                "an embedded header: {!r}".format(value))
+            raise HeaderParseError(
+                "header value appears to contain " "an embedded header: {!r}".format(value)
+            )
         return value
 
     def _normalize(self):
@@ -414,7 +420,6 @@ def _normalize(self):
         self._chunks = chunks
 
 
-
 class _ValueFormatter:
     def __init__(self, headerlen, maxlen, continuation_ws, splitchars):
         self._maxlen = maxlen
@@ -433,7 +438,7 @@ def __str__(self):
 
     def newline(self):
         end_of_line = self._current_line.pop()
-        if end_of_line != (' ', ''):
+        if end_of_line != (" ", ""):
             self._current_line.push(*end_of_line)
         if len(self._current_line) > 0:
             if self._current_line.is_onlyws():
@@ -443,7 +448,7 @@ def newline(self):
         self._current_line.reset()
 
     def add_transition(self):
-        self._current_line.push(' ', '')
+        self._current_line.push(" ", "")
 
     def feed(self, fws, string, charset):
         # If the charset has no header encoding (i.e. it is an ASCII encoding)
@@ -502,12 +507,12 @@ def _ascii_split(self, fws, string, splitchars):
         # where we would sometimes *introduce* FWS after a splitchar, or the
         # algorithm before that, where we would turn all white space runs into
         # single spaces or tabs.)
-        parts = re.split("(["+FWS+"]+)", fws+string)
+        parts = re.split("([" + FWS + "]+)", fws + string)
         if parts[0]:
-            parts[:0] = ['']
+            parts[:0] = [""]
         else:
             parts.pop(0)
-        for fws, part in zip(*[iter(parts)]*2):
+        for fws, part in zip(*[iter(parts)] * 2):
             self._append_chunk(fws, part)
 
     def _append_chunk(self, fws, string):
@@ -516,13 +521,13 @@ def _append_chunk(self, fws, string):
             # Find the best split point, working backward from the end.
             # There might be none, on a long first line.
             for ch in self._splitchars:
-                for i in range(self._current_line.part_count()-1, 0, -1):
+                for i in range(self._current_line.part_count() - 1, 0, -1):
                     if ch.isspace():
                         fws = self._current_line[i][0]
-                        if fws and fws[0]==ch:
+                        if fws and fws[0] == ch:
                             break
-                    prevpart = self._current_line[i-1][1]
-                    if prevpart and prevpart[-1]==ch:
+                    prevpart = self._current_line[i - 1][1]
+                    if prevpart and prevpart[-1] == ch:
                         break
                 else:
                     continue
@@ -535,7 +540,7 @@ def _append_chunk(self, fws, string):
                     if not fws:
                         # We don't use continuation_ws here because the whitespace
                         # after a header should always be a space.
-                        fws = ' '
+                        fws = " "
                 self._current_line.push(fws, part)
                 return
             remainder = self._current_line.pop_from(i)
@@ -544,7 +549,6 @@ def _append_chunk(self, fws, string):
 
 
 class _Accumulator(list):
-
     def __init__(self, initial_size=0):
         self._initial_size = initial_size
         super().__init__()
@@ -558,17 +562,15 @@ def pop_from(self, i=0):
         return popped
 
     def pop(self):
-        if self.part_count()==0:
-            return ('', '')
+        if self.part_count() == 0:
+            return ("", "")
         return super().pop()
 
     def __len__(self):
-        return sum((len(fws)+len(part) for fws, part in self),
-                   self._initial_size)
+        return sum((len(fws) + len(part) for fws, part in self), self._initial_size)
 
     def __str__(self):
-        return EMPTYSTRING.join((EMPTYSTRING.join((fws, part))
-                                for fws, part in self))
+        return EMPTYSTRING.join((EMPTYSTRING.join((fws, part)) for fws, part in self))
 
     def reset(self, startval=None):
         if startval is None:
@@ -577,7 +579,7 @@ def reset(self, startval=None):
         self._initial_size = 0
 
     def is_onlyws(self):
-        return self._initial_size==0 and (not self or str(self).isspace())
+        return self._initial_size == 0 and (not self or str(self).isspace())
 
     def part_count(self):
         return super().__len__()
diff --git a/unix-ffi/email.header/manifest.py b/unix-ffi/email.header/manifest.py
new file mode 100644
index 000000000..0be7e85c2
--- /dev/null
+++ b/unix-ffi/email.header/manifest.py
@@ -0,0 +1,9 @@
+metadata(version="0.5.2")
+
+require("re")
+require("binascii")
+require("email.encoders")
+require("email.errors")
+require("email.charset")
+
+package("email")
diff --git a/email.internal/email/_encoded_words.py b/unix-ffi/email.internal/email/_encoded_words.py
similarity index 76%
rename from email.internal/email/_encoded_words.py
rename to unix-ffi/email.internal/email/_encoded_words.py
index 9e0cc75b0..683ff3c52 100644
--- a/email.internal/email/_encoded_words.py
+++ b/unix-ffi/email.internal/email/_encoded_words.py
@@ -46,33 +46,35 @@
 from string import ascii_letters, digits
 from email import errors
 
-__all__ = ['decode_q',
-           'encode_q',
-           'decode_b',
-           'encode_b',
-           'len_q',
-           'len_b',
-           'decode',
-           'encode',
-           ]
+__all__ = [
+    "decode_q",
+    "encode_q",
+    "decode_b",
+    "encode_b",
+    "len_q",
+    "len_b",
+    "decode",
+    "encode",
+]
 
 #
 # Quoted Printable
 #
 
 # regex based decoder.
-_q_byte_subber = functools.partial(re.compile(br'=([a-fA-F0-9]{2})').sub,
-        lambda m: bytes([int(m.group(1), 16)]))
+_q_byte_subber = functools.partial(
+    re.compile(rb"=([a-fA-F0-9]{2})").sub, lambda m: bytes([int(m.group(1), 16)])
+)
+
 
 def decode_q(encoded):
-    encoded = encoded.replace(b'_', b' ')
+    encoded = encoded.replace(b"_", b" ")
     return _q_byte_subber(encoded), []
 
 
 # dict mapping bytes to their encoded form
 class _QByteMap(dict):
-
-    safe = b'-!*+/' + ascii_letters.encode('ascii') + digits.encode('ascii')
+    safe = b"-!*+/" + ascii_letters.encode("ascii") + digits.encode("ascii")
 
     def __missing__(self, key):
         if key in self.safe:
@@ -81,13 +83,16 @@ def __missing__(self, key):
             self[key] = "={:02X}".format(key)
         return self[key]
 
+
 _q_byte_map = _QByteMap()
 
 # In headers spaces are mapped to '_'.
-_q_byte_map[ord(' ')] = '_'
+_q_byte_map[ord(" ")] = "_"
+
 
 def encode_q(bstring):
-    return ''.join(_q_byte_map[x] for x in bstring)
+    return "".join(_q_byte_map[x] for x in bstring)
+
 
 def len_q(bstring):
     return sum(len(_q_byte_map[x]) for x in bstring)
@@ -97,12 +102,13 @@ def len_q(bstring):
 # Base64
 #
 
+
 def decode_b(encoded):
     defects = []
     pad_err = len(encoded) % 4
     if pad_err:
         defects.append(errors.InvalidBase64PaddingDefect())
-        padded_encoded = encoded + b'==='[:4-pad_err]
+        padded_encoded = encoded + b"==="[: 4 - pad_err]
     else:
         padded_encoded = encoded
     try:
@@ -115,16 +121,18 @@ def decode_b(encoded):
         # try various padding lengths until something works.
         for i in 0, 1, 2, 3:
             try:
-                return base64.b64decode(encoded+b'='*i, validate=False), defects
+                return base64.b64decode(encoded + b"=" * i, validate=False), defects
             except binascii.Error:
-                if i==0:
+                if i == 0:
                     defects.append(errors.InvalidBase64PaddingDefect())
         else:
             # This should never happen.
             raise AssertionError("unexpected binascii.Error")
 
+
 def encode_b(bstring):
-    return base64.b64encode(bstring).decode('ascii')
+    return base64.b64encode(bstring).decode("ascii")
+
 
 def len_b(bstring):
     groups_of_3, leftover = divmod(len(bstring), 3)
@@ -133,9 +141,10 @@ def len_b(bstring):
 
 
 _cte_decoders = {
-    'q': decode_q,
-    'b': decode_b,
-    }
+    "q": decode_q,
+    "b": decode_b,
+}
+
 
 def decode(ew):
     """Decode encoded word and return (string, charset, lang, defects) tuple.
@@ -158,38 +167,46 @@ def decode(ew):
     which is rarely if ever encountered, is the empty string.
 
     """
-    _, charset, cte, cte_string, _ = ew.split('?')
-    charset, _, lang = charset.partition('*')
+    _, charset, cte, cte_string, _ = ew.split("?")
+    charset, _, lang = charset.partition("*")
     cte = cte.lower()
     # Recover the original bytes and do CTE decoding.
-    bstring = cte_string.encode('ascii', 'surrogateescape')
+    bstring = cte_string.encode("ascii", "surrogateescape")
     bstring, defects = _cte_decoders[cte](bstring)
     # Turn the CTE decoded bytes into unicode.
     try:
         string = bstring.decode(charset)
     except UnicodeError:
-        defects.append(errors.UndecodableBytesDefect("Encoded word "
-            "contains bytes not decodable using {} charset".format(charset)))
-        string = bstring.decode(charset, 'surrogateescape')
+        defects.append(
+            errors.UndecodableBytesDefect(
+                "Encoded word " "contains bytes not decodable using {} charset".format(charset)
+            )
+        )
+        string = bstring.decode(charset, "surrogateescape")
     except LookupError:
-        string = bstring.decode('ascii', 'surrogateescape')
-        if charset.lower() != 'unknown-8bit':
-            defects.append(errors.CharsetError("Unknown charset {} "
-                "in encoded word; decoded as unknown bytes".format(charset)))
+        string = bstring.decode("ascii", "surrogateescape")
+        if charset.lower() != "unknown-8bit":
+            defects.append(
+                errors.CharsetError(
+                    "Unknown charset {} "
+                    "in encoded word; decoded as unknown bytes".format(charset)
+                )
+            )
     return string, charset, lang, defects
 
 
 _cte_encoders = {
-    'q': encode_q,
-    'b': encode_b,
-    }
+    "q": encode_q,
+    "b": encode_b,
+}
 
 _cte_encode_length = {
-    'q': len_q,
-    'b': len_b,
-    }
+    "q": len_q,
+    "b": len_b,
+}
+
 
-def encode(string, charset='utf-8', encoding=None, lang=''):
+def encode(string, charset="utf-8", encoding=None, lang=""):
     """Encode string using the CTE encoding that produces the shorter result.
 
     Produces an RFC 2047/2243 encoded word of the form:
@@ -206,16 +223,16 @@ def encode(string, charset='utf-8', encoding=None, lang=''):
     RFC 2243 language string to specify in the encoded word.
 
     """
-    if charset == 'unknown-8bit':
-        bstring = string.encode('ascii', 'surrogateescape')
+    if charset == "unknown-8bit":
+        bstring = string.encode("ascii", "surrogateescape")
     else:
         bstring = string.encode(charset)
     if encoding is None:
-        qlen = _cte_encode_length['q'](bstring)
-        blen = _cte_encode_length['b'](bstring)
+        qlen = _cte_encode_length["q"](bstring)
+        blen = _cte_encode_length["b"](bstring)
         # Bias toward q.  5 is arbitrary.
-        encoding = 'q' if qlen - blen < 5 else 'b'
+        encoding = "q" if qlen - blen < 5 else "b"
     encoded = _cte_encoders[encoding](bstring)
     if lang:
-        lang = '*' + lang
+        lang = "*" + lang
     return "=?{}{}?{}?{}?=".format(charset, lang, encoding, encoded)
diff --git a/email.internal/email/_parseaddr.py b/unix-ffi/email.internal/email/_parseaddr.py
similarity index 82%
rename from email.internal/email/_parseaddr.py
rename to unix-ffi/email.internal/email/_parseaddr.py
index cdfa3729a..93373d679 100644
--- a/email.internal/email/_parseaddr.py
+++ b/unix-ffi/email.internal/email/_parseaddr.py
@@ -7,25 +7,47 @@
 """
 
 __all__ = [
-    'mktime_tz',
-    'parsedate',
-    'parsedate_tz',
-    'quote',
-    ]
+    "mktime_tz",
+    "parsedate",
+    "parsedate_tz",
+    "quote",
+]
 
 import time, calendar
 
-SPACE = ' '
-EMPTYSTRING = ''
-COMMASPACE = ', '
+SPACE = " "
+EMPTYSTRING = ""
+COMMASPACE = ", "
 
 # Parse a date field
-_monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul',
-               'aug', 'sep', 'oct', 'nov', 'dec',
-               'january', 'february', 'march', 'april', 'may', 'june', 'july',
-               'august', 'september', 'october', 'november', 'december']
-
-_daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
+_monthnames = [
+    "jan",
+    "feb",
+    "mar",
+    "apr",
+    "may",
+    "jun",
+    "jul",
+    "aug",
+    "sep",
+    "oct",
+    "nov",
+    "dec",
+    "january",
+    "february",
+    "march",
+    "april",
+    "may",
+    "june",
+    "july",
+    "august",
+    "september",
+    "october",
+    "november",
+    "december",
+]
+
+_daynames = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
 
 # The timezone table does not include the military time zones defined
 # in RFC822, other than Z.  According to RFC1123, the description in
@@ -33,13 +55,22 @@
 # zones.  RFC1123 recommends that numeric timezone indicators be used
 # instead of timezone names.
 
-_timezones = {'UT':0, 'UTC':0, 'GMT':0, 'Z':0,
-              'AST': -400, 'ADT': -300,  # Atlantic (used in Canada)
-              'EST': -500, 'EDT': -400,  # Eastern
-              'CST': -600, 'CDT': -500,  # Central
-              'MST': -700, 'MDT': -600,  # Mountain
-              'PST': -800, 'PDT': -700   # Pacific
-              }
+_timezones = {
+    "UT": 0,
+    "UTC": 0,
+    "GMT": 0,
+    "Z": 0,
+    "AST": -400,
+    "ADT": -300,  # Atlantic (used in Canada)
+    "EST": -500,
+    "EDT": -400,  # Eastern
+    "CST": -600,
+    "CDT": -500,  # Central
+    "MST": -700,
+    "MDT": -600,  # Mountain
+    "PST": -800,
+    "PDT": -700,  # Pacific
+}
 
 
 def parsedate_tz(data):
@@ -54,6 +85,7 @@ def parsedate_tz(data):
         res[9] = 0
     return tuple(res)
 
+
 def _parsedate_tz(data):
     """Convert date to extended time tuple.
 
@@ -69,26 +101,26 @@ def _parsedate_tz(data):
     data = data.split()
     # The FWS after the comma after the day-of-week is optional, so search and
     # adjust for this.
-    if data[0].endswith(',') or data[0].lower() in _daynames:
+    if data[0].endswith(",") or data[0].lower() in _daynames:
         # There's a dayname here. Skip it
         del data[0]
     else:
-        i = data[0].rfind(',')
+        i = data[0].rfind(",")
         if i >= 0:
-            data[0] = data[0][i+1:]
-    if len(data) == 3: # RFC 850 date, deprecated
-        stuff = data[0].split('-')
+            data[0] = data[0][i + 1 :]
+    if len(data) == 3:  # RFC 850 date, deprecated
+        stuff = data[0].split("-")
         if len(stuff) == 3:
             data = stuff + data[1:]
     if len(data) == 4:
         s = data[3]
-        i = s.find('+')
+        i = s.find("+")
         if i == -1:
-            i = s.find('-')
+            i = s.find("-")
         if i > 0:
             data[3:] = [s[:i], s[i:]]
         else:
-            data.append('') # Dummy tz
+            data.append("")  # Dummy tz
     if len(data) < 5:
         return None
     data = data[:5]
@@ -101,26 +133,26 @@ def _parsedate_tz(data):
     mm = _monthnames.index(mm) + 1
     if mm > 12:
         mm -= 12
-    if dd[-1] == ',':
+    if dd[-1] == ",":
         dd = dd[:-1]
-    i = yy.find(':')
+    i = yy.find(":")
     if i > 0:
         yy, tm = tm, yy
-    if yy[-1] == ',':
+    if yy[-1] == ",":
         yy = yy[:-1]
     if not yy[0].isdigit():
         yy, tz = tz, yy
-    if tm[-1] == ',':
+    if tm[-1] == ",":
         tm = tm[:-1]
-    tm = tm.split(':')
+    tm = tm.split(":")
     if len(tm) == 2:
         [thh, tmm] = tm
-        tss = '0'
+        tss = "0"
     elif len(tm) == 3:
         [thh, tmm, tss] = tm
-    elif len(tm) == 1 and '.' in tm[0]:
+    elif len(tm) == 1 and "." in tm[0]:
         # Some non-compliant MUAs use '.' to separate time elements.
-        tm = tm[0].split('.')
+        tm = tm[0].split(".")
         if len(tm) == 2:
             [thh, tmm] = tm
             tss = 0
@@ -157,7 +189,7 @@ def _parsedate_tz(data):
             tzoffset = int(tz)
         except ValueError:
             pass
-        if tzoffset==0 and tz.startswith('-'):
+        if tzoffset == 0 and tz.startswith("-"):
             tzoffset = None
     # Convert a timezone offset into seconds ; -0500 -> -18000
     if tzoffset:
@@ -166,7 +198,7 @@ def _parsedate_tz(data):
             tzoffset = -tzoffset
         else:
             tzsign = 1
-        tzoffset = tzsign * ( (tzoffset//100)*3600 + (tzoffset % 100)*60)
+        tzoffset = tzsign * ((tzoffset // 100) * 3600 + (tzoffset % 100) * 60)
     # Daylight Saving Time flag is set to -1, since DST is unknown.
     return [yy, mm, dd, thh, tmm, tss, 0, 1, -1, tzoffset]
 
@@ -197,7 +229,7 @@ def quote(str):
     are the only characters that need to be quoted inside a quoted string.
     Does not add the surrounding double quotes.
     """
-    return str.replace('\\', '\\\\').replace('"', '\\"')
+    return str.replace("\\", "\\\\").replace('"', '\\"')
 
 
 class AddrlistClass:
@@ -216,16 +248,16 @@ def __init__(self, field):
         `field' is an unparsed address header field, containing
         one or more addresses.
         """
-        self.specials = '()<>@,:;.\"[]'
+        self.specials = '()<>@,:;."[]'
         self.pos = 0
-        self.LWS = ' \t'
-        self.CR = '\r\n'
+        self.LWS = " \t"
+        self.CR = "\r\n"
         self.FWS = self.LWS + self.CR
         self.atomends = self.specials + self.LWS + self.CR
         # Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it
         # is obsolete syntax.  RFC 2822 requires that we recognize obsolete
         # syntax, so allow dots in phrases.
-        self.phraseends = self.atomends.replace('.', '')
+        self.phraseends = self.atomends.replace(".", "")
         self.field = field
         self.commentlist = []
 
@@ -233,11 +265,11 @@ def gotonext(self):
         """Skip white space and extract comments."""
         wslist = []
         while self.pos < len(self.field):
-            if self.field[self.pos] in self.LWS + '\n\r':
-                if self.field[self.pos] not in '\n\r':
+            if self.field[self.pos] in self.LWS + "\n\r":
+                if self.field[self.pos] not in "\n\r":
                     wslist.append(self.field[self.pos])
                 self.pos += 1
-            elif self.field[self.pos] == '(':
+            elif self.field[self.pos] == "(":
                 self.commentlist.append(self.getcomment())
             else:
                 break
@@ -254,7 +286,7 @@ def getaddrlist(self):
             if ad:
                 result += ad
             else:
-                result.append(('', ''))
+                result.append(("", ""))
         return result
 
     def getaddress(self):
@@ -274,7 +306,7 @@ def getaddress(self):
             if plist:
                 returnlist = [(SPACE.join(self.commentlist), plist[0])]
 
-        elif self.field[self.pos] in '.@':
+        elif self.field[self.pos] in ".@":
             # email address is just an addrspec
             # this isn't very efficient since we start over
             self.pos = oldpos
@@ -282,7 +314,7 @@ def getaddress(self):
             addrspec = self.getaddrspec()
             returnlist = [(SPACE.join(self.commentlist), addrspec)]
 
-        elif self.field[self.pos] == ':':
+        elif self.field[self.pos] == ":":
             # address is a group
             returnlist = []
 
@@ -290,18 +322,19 @@ def getaddress(self):
             self.pos += 1
             while self.pos < len(self.field):
                 self.gotonext()
-                if self.pos < fieldlen and self.field[self.pos] == ';':
+                if self.pos < fieldlen and self.field[self.pos] == ";":
                     self.pos += 1
                     break
                 returnlist = returnlist + self.getaddress()
 
-        elif self.field[self.pos] == '<':
+        elif self.field[self.pos] == "<":
             # Address is a phrase then a route addr
             routeaddr = self.getrouteaddr()
 
             if self.commentlist:
-                returnlist = [(SPACE.join(plist) + ' (' +
-                               ' '.join(self.commentlist) + ')', routeaddr)]
+                returnlist = [
+                    (SPACE.join(plist) + " (" + " ".join(self.commentlist) + ")", routeaddr)
+                ]
             else:
                 returnlist = [(SPACE.join(plist), routeaddr)]
 
@@ -312,7 +345,7 @@ def getaddress(self):
                 self.pos += 1
 
         self.gotonext()
-        if self.pos < len(self.field) and self.field[self.pos] == ',':
+        if self.pos < len(self.field) and self.field[self.pos] == ",":
             self.pos += 1
         return returnlist
 
@@ -321,24 +354,24 @@ def getrouteaddr(self):
 
         This method just skips all the route stuff and returns the addrspec.
         """
-        if self.field[self.pos] != '<':
+        if self.field[self.pos] != "<":
             return
 
         expectroute = False
         self.pos += 1
         self.gotonext()
-        adlist = ''
+        adlist = ""
         while self.pos < len(self.field):
             if expectroute:
                 self.getdomain()
                 expectroute = False
-            elif self.field[self.pos] == '>':
+            elif self.field[self.pos] == ">":
                 self.pos += 1
                 break
-            elif self.field[self.pos] == '@':
+            elif self.field[self.pos] == "@":
                 self.pos += 1
                 expectroute = True
-            elif self.field[self.pos] == ':':
+            elif self.field[self.pos] == ":":
                 self.pos += 1
             else:
                 adlist = self.getaddrspec()
@@ -355,10 +388,10 @@ def getaddrspec(self):
         self.gotonext()
         while self.pos < len(self.field):
             preserve_ws = True
-            if self.field[self.pos] == '.':
+            if self.field[self.pos] == ".":
                 if aslist and not aslist[-1].strip():
                     aslist.pop()
-                aslist.append('.')
+                aslist.append(".")
                 self.pos += 1
                 preserve_ws = False
             elif self.field[self.pos] == '"':
@@ -373,10 +406,10 @@ def getaddrspec(self):
             if preserve_ws and ws:
                 aslist.append(ws)
 
-        if self.pos >= len(self.field) or self.field[self.pos] != '@':
+        if self.pos >= len(self.field) or self.field[self.pos] != "@":
             return EMPTYSTRING.join(aslist)
 
-        aslist.append('@')
+        aslist.append("@")
         self.pos += 1
         self.gotonext()
         return EMPTYSTRING.join(aslist) + self.getdomain()
@@ -387,13 +420,13 @@ def getdomain(self):
         while self.pos < len(self.field):
             if self.field[self.pos] in self.LWS:
                 self.pos += 1
-            elif self.field[self.pos] == '(':
+            elif self.field[self.pos] == "(":
                 self.commentlist.append(self.getcomment())
-            elif self.field[self.pos] == '[':
+            elif self.field[self.pos] == "[":
                 sdlist.append(self.getdomainliteral())
-            elif self.field[self.pos] == '.':
+            elif self.field[self.pos] == ".":
                 self.pos += 1
-                sdlist.append('.')
+                sdlist.append(".")
             elif self.field[self.pos] in self.atomends:
                 break
             else:
@@ -414,9 +447,9 @@ def getdelimited(self, beginchar, endchars, allowcomments=True):
         within the parsed fragment.
         """
         if self.field[self.pos] != beginchar:
-            return ''
+            return ""
 
-        slist = ['']
+        slist = [""]
         quote = False
         self.pos += 1
         while self.pos < len(self.field):
@@ -426,10 +459,10 @@ def getdelimited(self, beginchar, endchars, allowcomments=True):
             elif self.field[self.pos] in endchars:
                 self.pos += 1
                 break
-            elif allowcomments and self.field[self.pos] == '(':
+            elif allowcomments and self.field[self.pos] == "(":
                 slist.append(self.getcomment())
-                continue        # have already advanced pos from getcomment
-            elif self.field[self.pos] == '\\':
+                continue  # have already advanced pos from getcomment
+            elif self.field[self.pos] == "\\":
                 quote = True
             else:
                 slist.append(self.field[self.pos])
@@ -443,11 +476,11 @@ def getquote(self):
 
     def getcomment(self):
         """Get a parenthesis-delimited fragment from self's field."""
-        return self.getdelimited('(', ')\r', True)
+        return self.getdelimited("(", ")\r", True)
 
     def getdomainliteral(self):
         """Parse an RFC 2822 domain-literal."""
-        return '[%s]' % self.getdelimited('[', ']\r', False)
+        return "[%s]" % self.getdelimited("[", "]\r", False)
 
     def getatom(self, atomends=None):
         """Parse an RFC 2822 atom.
@@ -456,7 +489,7 @@ def getatom(self, atomends=None):
         (the default is to use self.atomends).  This is used e.g. in
         getphraselist() since phrase endings must not include the `.' (which
         is legal in phrases)."""
-        atomlist = ['']
+        atomlist = [""]
         if atomends is None:
             atomends = self.atomends
 
@@ -483,7 +516,7 @@ def getphraselist(self):
                 self.pos += 1
             elif self.field[self.pos] == '"':
                 plist.append(self.getquote())
-            elif self.field[self.pos] == '(':
+            elif self.field[self.pos] == "(":
                 self.commentlist.append(self.getcomment())
             elif self.field[self.pos] in self.phraseends:
                 break
@@ -492,8 +525,10 @@ def getphraselist(self):
 
         return plist
 
+
 class AddressList(AddrlistClass):
     """An AddressList encapsulates a list of parsed RFC 2822 addresses."""
+
     def __init__(self, field):
         AddrlistClass.__init__(self, field)
         if field:
diff --git a/email.internal/email/_policybase.py b/unix-ffi/email.internal/email/_policybase.py
similarity index 89%
rename from email.internal/email/_policybase.py
rename to unix-ffi/email.internal/email/_policybase.py
index bb533220e..7e93ca4de 100644
--- a/email.internal/email/_policybase.py
+++ b/unix-ffi/email.internal/email/_policybase.py
@@ -9,10 +9,10 @@
 from email.utils import _has_surrogates
 
 __all__ = [
-    'Policy',
-    'Compat32',
-    'compat32',
-    ]
+    "Policy",
+    "Compat32",
+    "compat32",
+]
 
 
 class _PolicyBase:
@@ -46,16 +46,17 @@ def __init__(self, **kw):
         """
         for name, value in kw.items():
             if hasattr(self, name):
-                super(_PolicyBase,self).__setattr__(name, value)
+                super(_PolicyBase, self).__setattr__(name, value)
             else:
                 raise TypeError(
                     "{!r} is an invalid keyword argument for {}".format(
-                        name, self.__class__.__name__))
+                        name, self.__class__.__name__
+                    )
+                )
 
     def __repr__(self):
-        args = [ "{}={!r}".format(name, value)
-                 for name, value in self.__dict__.items() ]
-        return "{}({})".format(self.__class__.__name__, ', '.join(args))
+        args = ["{}={!r}".format(name, value) for name, value in self.__dict__.items()]
+        return "{}({})".format(self.__class__.__name__, ", ".join(args))
 
     def clone(self, **kw):
         """Return a new instance with specified attributes changed.
@@ -71,7 +72,9 @@ def clone(self, **kw):
             if not hasattr(self, attr):
                 raise TypeError(
                     "{!r} is an invalid keyword argument for {}".format(
-                        attr, self.__class__.__name__))
+                        attr, self.__class__.__name__
+                    )
+                )
             object.__setattr__(newpolicy, attr, value)
         return newpolicy
 
@@ -92,26 +95,26 @@ def __add__(self, other):
 
 
 def _append_doc(doc, added_doc):
-    doc = doc.rsplit('\n', 1)[0]
-    added_doc = added_doc.split('\n', 1)[1]
-    return doc + '\n' + added_doc
+    doc = doc.rsplit("\n", 1)[0]
+    added_doc = added_doc.split("\n", 1)[1]
+    return doc + "\n" + added_doc
+
 
 def _extend_docstrings(cls):
     return cls
-    if cls.__doc__ and cls.__doc__.startswith('+'):
+    if cls.__doc__ and cls.__doc__.startswith("+"):
         cls.__doc__ = _append_doc(cls.__bases__[0].__doc__, cls.__doc__)
     for name, attr in cls.__dict__.items():
-        if attr.__doc__ and attr.__doc__.startswith('+'):
+        if attr.__doc__ and attr.__doc__.startswith("+"):
             for c in (c for base in cls.__bases__ for c in base.mro()):
-                doc = getattr(getattr(c, name), '__doc__')
+                doc = getattr(getattr(c, name), "__doc__")
                 if doc:
                     attr.__doc__ = _append_doc(doc, attr.__doc__)
                     break
     return cls
 
 
-class Policy(_PolicyBase):#, metaclass=abc.ABCMeta):
-
+class Policy(_PolicyBase):  # , metaclass=abc.ABCMeta):
     r"""Controls for how messages are interpreted and formatted.
 
     Most of the classes and many of the methods in the email package accept
@@ -153,8 +156,8 @@ class Policy(_PolicyBase):#, metaclass=abc.ABCMeta):
     """
 
     raise_on_defect = False
-    linesep = '\n'
-    cte_type = '8bit'
+    linesep = "\n"
+    cte_type = "8bit"
     max_line_length = 78
 
     def handle_defect(self, obj, defect):
@@ -274,8 +277,7 @@ def _sanitize_header(self, name, value):
             # Assume it is already a header object
             return value
         if _has_surrogates(value):
-            return header.Header(value, charset=_charset.UNKNOWN8BIT,
-                                 header_name=name)
+            return header.Header(value, charset=_charset.UNKNOWN8BIT, header_name=name)
         else:
             return value
 
@@ -287,9 +289,9 @@ def header_source_parse(self, sourcelines):
         stripping any trailing carriage return or linefeed characters.
 
         """
-        name, value = sourcelines[0].split(':', 1)
-        value = value.lstrip(' \t') + ''.join(sourcelines[1:])
-        return (name, value.rstrip('\r\n'))
+        name, value = sourcelines[0].split(":", 1)
+        value = value.lstrip(" \t") + "".join(sourcelines[1:])
+        return (name, value.rstrip("\r\n"))
 
     def header_store_parse(self, name, value):
         """+
@@ -323,18 +325,16 @@ def fold_binary(self, name, value):
         header is used, with its existing line breaks and/or binary data.
 
         """
-        folded = self._fold(name, value, sanitize=self.cte_type=='7bit')
-        return folded.encode('ascii', 'surrogateescape')
+        folded = self._fold(name, value, sanitize=self.cte_type == "7bit")
+        return folded.encode("ascii", "surrogateescape")
 
     def _fold(self, name, value, sanitize):
         parts = []
-        parts.append('%s: ' % name)
+        parts.append("%s: " % name)
         if isinstance(value, str):
             if _has_surrogates(value):
                 if sanitize:
-                    h = header.Header(value,
-                                      charset=_charset.UNKNOWN8BIT,
-                                      header_name=name)
+                    h = header.Header(value, charset=_charset.UNKNOWN8BIT, header_name=name)
                 else:
                     # If we have raw 8bit data in a byte string, we have no idea
                     # what the encoding is.  There is no safe way to split this
@@ -350,10 +350,9 @@ def _fold(self, name, value, sanitize):
             # Assume it is a Header-like object.
             h = value
         if h is not None:
-            parts.append(h.encode(linesep=self.linesep,
-                                  maxlinelen=self.max_line_length))
+            parts.append(h.encode(linesep=self.linesep, maxlinelen=self.max_line_length))
         parts.append(self.linesep)
-        return ''.join(parts)
+        return "".join(parts)
 
 
 compat32 = Compat32()
diff --git a/unix-ffi/email.internal/manifest.py b/unix-ffi/email.internal/manifest.py
new file mode 100644
index 000000000..88acb2c01
--- /dev/null
+++ b/unix-ffi/email.internal/manifest.py
@@ -0,0 +1,15 @@
+metadata(version="0.5.1")
+
+require("re")
+require("base64")
+require("binascii")
+require("functools")
+require("string")
+# require("calendar") TODO
+require("abc")
+require("email.errors")
+require("email.header")
+require("email.charset")
+require("email.utils")
+
+package("email")
diff --git a/email.message/email/iterators.py b/unix-ffi/email.message/email/iterators.py
similarity index 84%
rename from email.message/email/iterators.py
rename to unix-ffi/email.message/email/iterators.py
index 3adc4a04b..e15cd6f6c 100644
--- a/email.message/email/iterators.py
+++ b/unix-ffi/email.message/email/iterators.py
@@ -5,17 +5,16 @@
 """Various types of useful iterators and generators."""
 
 __all__ = [
-    'body_line_iterator',
-    'typed_subpart_iterator',
-    'walk',
+    "body_line_iterator",
+    "typed_subpart_iterator",
+    "walk",
     # Do not include _structure() since it's part of the debugging API.
-    ]
+]
 
 import sys
 from io import StringIO
 
 
-
 # This function will become a method of the Message class
 def walk(self):
     """Walk over the message tree, yielding each subpart.
@@ -30,7 +29,6 @@ def walk(self):
                 yield subsubpart
 
 
-
 # These two functions are imported into the Iterators.py interface module.
 def body_line_iterator(msg, decode=False):
     """Iterate over the parts, returning string payloads line-by-line.
@@ -44,7 +42,7 @@ def body_line_iterator(msg, decode=False):
                 yield line
 
 
-def typed_subpart_iterator(msg, maintype='text', subtype=None):
+def typed_subpart_iterator(msg, maintype="text", subtype=None):
     """Iterate over the subparts with a given MIME type.
 
     Use `maintype' as the main MIME type to match against; this defaults to
@@ -57,17 +55,16 @@ def typed_subpart_iterator(msg, maintype='text', subtype=None):
                 yield subpart
 
 
-
 def _structure(msg, fp=None, level=0, include_default=False):
     """A handy debugging aid"""
     if fp is None:
         fp = sys.stdout
-    tab = ' ' * (level * 4)
-    print(tab + msg.get_content_type(), end='', file=fp)
+    tab = " " * (level * 4)
+    print(tab + msg.get_content_type(), end="", file=fp)
     if include_default:
-        print(' [%s]' % msg.get_default_type(), file=fp)
+        print(" [%s]" % msg.get_default_type(), file=fp)
     else:
         print(file=fp)
     if msg.is_multipart():
         for subpart in msg.get_payload():
-            _structure(subpart, fp, level+1, include_default)
+            _structure(subpart, fp, level + 1, include_default)
diff --git a/email.message/email/message.py b/unix-ffi/email.message/email/message.py
similarity index 87%
rename from email.message/email/message.py
rename to unix-ffi/email.message/email/message.py
index 5020a0325..b669e5c1a 100644
--- a/email.message/email/message.py
+++ b/unix-ffi/email.message/email/message.py
@@ -4,7 +4,7 @@
 
 """Basic message object for the email package object model."""
 
-__all__ = ['Message']
+__all__ = ["Message"]
 
 import re
 import uu
@@ -18,9 +18,10 @@
 from email._policybase import compat32
 from email import charset as _charset
 from email._encoded_words import decode_b
+
 Charset = _charset.Charset
 
-SEMISPACE = '; '
+SEMISPACE = "; "
 
 # Regular expression that matches `special' characters in parameters, the
 # existence of which force quoting of the parameter value.
@@ -32,11 +33,12 @@ def _splitparam(param):
     # strictly RFC 2045 (section 5.1) compliant, but it catches most headers
     # found in the wild.  We may eventually need a full fledged parser.
     # RDM: we might have a Header here; for now just stringify it.
-    a, sep, b = str(param).partition(';')
+    a, sep, b = str(param).partition(";")
     if not sep:
         return a.strip(), None
     return a.strip(), b.strip()
-
+
+
 def _formatparam(param, value=None, quote=True):
     """Convenience function to format and return a key=value pair.
 
@@ -52,40 +54,41 @@ def _formatparam(param, value=None, quote=True):
         # instance.  RFC 2231 encoded values are never quoted, per RFC.
         if isinstance(value, tuple):
             # Encode as per RFC 2231
-            param += '*'
+            param += "*"
             value = utils.encode_rfc2231(value[2], value[0], value[1])
-            return '%s=%s' % (param, value)
+            return "%s=%s" % (param, value)
         else:
             try:
-                value.encode('ascii')
+                value.encode("ascii")
             except UnicodeEncodeError:
-                param += '*'
-                value = utils.encode_rfc2231(value, 'utf-8', '')
-                return '%s=%s' % (param, value)
+                param += "*"
+                value = utils.encode_rfc2231(value, "utf-8", "")
+                return "%s=%s" % (param, value)
         # BAW: Please check this.  I think that if quote is set it should
         # force quoting even if not necessary.
         if quote or tspecials.search(value):
             return '%s="%s"' % (param, utils.quote(value))
         else:
-            return '%s=%s' % (param, value)
+            return "%s=%s" % (param, value)
     else:
         return param
 
+
 def _parseparam(s):
     # RDM This might be a Header, so for now stringify it.
-    s = ';' + str(s)
+    s = ";" + str(s)
     plist = []
-    while s[:1] == ';':
+    while s[:1] == ";":
         s = s[1:]
-        end = s.find(';')
+        end = s.find(";")
         while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
-            end = s.find(';', end + 1)
+            end = s.find(";", end + 1)
         if end < 0:
             end = len(s)
         f = s[:end]
-        if '=' in f:
-            i = f.index('=')
-            f = f[:i].strip().lower() + '=' + f[i+1:].strip()
+        if "=" in f:
+            i = f.index("=")
+            f = f[:i].strip().lower() + "=" + f[i + 1 :].strip()
         plist.append(f.strip())
         s = s[end:]
     return plist
@@ -102,7 +105,6 @@ def _unquotevalue(value):
         return utils.unquote(value)
 
 
-
 class Message:
     """Basic message object.
 
@@ -118,6 +120,7 @@ class Message:
     you must use the explicit API to set or get all the headers.  Not all of
     the mapping methods are implemented.
     """
+
     def __init__(self, policy=compat32):
         self.policy = policy
         self._headers = []
@@ -128,7 +131,7 @@ def __init__(self, policy=compat32):
         self.preamble = self.epilogue = None
         self.defects = []
         # Default content type
-        self._default_type = 'text/plain'
+        self._default_type = "text/plain"
 
     def __str__(self):
         """Return the entire formatted message as a string.
@@ -146,6 +149,7 @@ def as_string(self, unixfrom=False, maxheaderlen=0):
         Generator instance.
         """
         from email.generator import Generator
+
         fp = StringIO()
         g = Generator(fp, mangle_from_=False, maxheaderlen=maxheaderlen)
         g.flatten(self, unixfrom=unixfrom)
@@ -223,40 +227,40 @@ def get_payload(self, i=None, decode=False):
         # For backward compatibility, Use isinstance and this error message
         # instead of the more logical is_multipart test.
         if i is not None and not isinstance(self._payload, list):
-            raise TypeError('Expected list, got %s' % type(self._payload))
+            raise TypeError("Expected list, got %s" % type(self._payload))
         payload = self._payload
         # cte might be a Header, so for now stringify it.
-        cte = str(self.get('content-transfer-encoding', '')).lower()
+        cte = str(self.get("content-transfer-encoding", "")).lower()
         # payload may be bytes here.
         if isinstance(payload, str):
             if utils._has_surrogates(payload):
-                bpayload = payload.encode('ascii', 'surrogateescape')
+                bpayload = payload.encode("ascii", "surrogateescape")
                 if not decode:
                     try:
-                        payload = bpayload.decode(self.get_param('charset', 'ascii'), 'replace')
+                        payload = bpayload.decode(self.get_param("charset", "ascii"), "replace")
                     except LookupError:
-                        payload = bpayload.decode('ascii', 'replace')
+                        payload = bpayload.decode("ascii", "replace")
             elif decode:
                 try:
-                    bpayload = payload.encode('ascii')
+                    bpayload = payload.encode("ascii")
                 except UnicodeError:
                     # This won't happen for RFC compliant messages (messages
                     # containing only ASCII codepoints in the unicode input).
                     # If it does happen, turn the string into bytes in a way
                     # guaranteed not to fail.
-                    bpayload = payload.encode('raw-unicode-escape')
+                    bpayload = payload.encode("raw-unicode-escape")
         if not decode:
             return payload
-        if cte == 'quoted-printable':
+        if cte == "quoted-printable":
             return utils._qdecode(bpayload)
-        elif cte == 'base64':
+        elif cte == "base64":
             # XXX: this is a bit of a hack; decode_b should probably be factored
             # out somewhere, but I haven't figured out where yet.
-            value, defects = decode_b(b''.join(bpayload.splitlines()))
+            value, defects = decode_b(b"".join(bpayload.splitlines()))
             for defect in defects:
                 self.policy.handle_defect(self, defect)
             return value
-        elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
+        elif cte in ("x-uuencode", "uuencode", "uue", "x-uue"):
             in_file = BytesIO(bpayload)
             out_file = BytesIO()
             try:
@@ -276,7 +280,7 @@ def set_payload(self, payload, charset=None):
         set_charset() for details.
         """
         if isinstance(payload, bytes):
-            payload = payload.decode('ascii', 'surrogateescape')
+            payload = payload.decode("ascii", "surrogateescape")
         self._payload = payload
         if charset is not None:
             self.set_charset(charset)
@@ -296,32 +300,30 @@ def set_charset(self, charset):
         Content-Type, Content-Transfer-Encoding) will be added as needed.
         """
         if charset is None:
-            self.del_param('charset')
+            self.del_param("charset")
             self._charset = None
             return
         if not isinstance(charset, Charset):
             charset = Charset(charset)
         self._charset = charset
-        if 'MIME-Version' not in self:
-            self.add_header('MIME-Version', '1.0')
-        if 'Content-Type' not in self:
-            self.add_header('Content-Type', 'text/plain',
-                            charset=charset.get_output_charset())
+        if "MIME-Version" not in self:
+            self.add_header("MIME-Version", "1.0")
+        if "Content-Type" not in self:
+            self.add_header("Content-Type", "text/plain", charset=charset.get_output_charset())
         else:
-            self.set_param('charset', charset.get_output_charset())
+            self.set_param("charset", charset.get_output_charset())
         if charset != charset.get_output_charset():
             self._payload = charset.body_encode(self._payload)
-        if 'Content-Transfer-Encoding' not in self:
+        if "Content-Transfer-Encoding" not in self:
             cte = charset.get_body_encoding()
             try:
                 cte(self)
             except TypeError:
                 self._payload = charset.body_encode(self._payload)
-                self.add_header('Content-Transfer-Encoding', cte)
+                self.add_header("Content-Transfer-Encoding", cte)
 
     def get_charset(self):
-        """Return the Charset instance associated with the message's payload.
-        """
+        """Return the Charset instance associated with the message's payload."""
         return self._charset
 
     #
@@ -356,8 +358,10 @@ def __setitem__(self, name, val):
                 if k.lower() == lname:
                     found += 1
                     if found >= max_count:
-                        raise ValueError("There may be at most {} {} headers "
-                                         "in a message".format(max_count, name))
+                        raise ValueError(
+                            "There may be at most {} {} headers "
+                            "in a message".format(max_count, name)
+                        )
         self._headers.append(self.policy.header_store_parse(name, val))
 
     def __delitem__(self, name):
@@ -397,8 +401,7 @@ def values(self):
         Any fields deleted and re-inserted are always appended to the header
         list.
         """
-        return [self.policy.header_fetch_parse(k, v)
-                for k, v in self._headers]
+        return [self.policy.header_fetch_parse(k, v) for k, v in self._headers]
 
     def items(self):
         """Get all the message's header fields and values.
@@ -408,8 +411,7 @@ def items(self):
         Any fields deleted and re-inserted are always appended to the header
         list.
         """
-        return [(k, self.policy.header_fetch_parse(k, v))
-                for k, v in self._headers]
+        return [(k, self.policy.header_fetch_parse(k, v)) for k, v in self._headers]
 
     def get(self, name, failobj=None):
         """Get a header value.
@@ -487,9 +489,9 @@ def add_header(self, _name, _value, **_params):
         parts = []
         for k, v in _params.items():
             if v is None:
-                parts.append(k.replace('_', '-'))
+                parts.append(k.replace("_", "-"))
             else:
-                parts.append(_formatparam(k.replace('_', '-'), v))
+                parts.append(_formatparam(k.replace("_", "-"), v))
         if _value is not None:
             parts.insert(0, _value)
         self[_name] = SEMISPACE.join(parts)
@@ -527,14 +529,14 @@ def get_content_type(self):
         message/rfc822.
         """
         missing = object()
-        value = self.get('content-type', missing)
+        value = self.get("content-type", missing)
         if value is missing:
             # This should have no parameters
             return self.get_default_type()
         ctype = _splitparam(value)[0].lower()
         # RFC 2045, section 5.2 says if its invalid, use text/plain
-        if ctype.count('/') != 1:
-            return 'text/plain'
+        if ctype.count("/") != 1:
+            return "text/plain"
         return ctype
 
     def get_content_maintype(self):
@@ -544,7 +546,7 @@ def get_content_maintype(self):
         get_content_type().
         """
         ctype = self.get_content_type()
-        return ctype.split('/')[0]
+        return ctype.split("/")[0]
 
     def get_content_subtype(self):
         """Returns the message's sub-content type.
@@ -553,7 +555,7 @@ def get_content_subtype(self):
         get_content_type().
         """
         ctype = self.get_content_type()
-        return ctype.split('/')[1]
+        return ctype.split("/")[1]
 
     def get_default_type(self):
         """Return the `default' content type.
@@ -583,18 +585,18 @@ def _get_params_preserve(self, failobj, header):
         params = []
         for p in _parseparam(value):
             try:
-                name, val = p.split('=', 1)
+                name, val = p.split("=", 1)
                 name = name.strip()
                 val = val.strip()
             except ValueError:
                 # Must have been a bare attribute
                 name = p.strip()
-                val = ''
+                val = ""
             params.append((name, val))
         params = utils.decode_params(params)
         return params
 
-    def get_params(self, failobj=None, header='content-type', unquote=True):
+    def get_params(self, failobj=None, header="content-type", unquote=True):
         """Return the message's Content-Type parameters, as a list.
 
         The elements of the returned list are 2-tuples of key/value pairs, as
@@ -616,8 +618,7 @@ def get_params(self, failobj=None, header='content-type', unquote=True):
         else:
             return params
 
-    def get_param(self, param, failobj=None, header='content-type',
-                  unquote=True):
+    def get_param(self, param, failobj=None, header="content-type", unquote=True):
         """Return the parameter value if found in the Content-Type header.
 
         Optional failobj is the object to return if there is no Content-Type
@@ -650,8 +651,9 @@ def get_param(self, param, failobj=None, header='content-type',
                     return v
         return failobj
 
-    def set_param(self, param, value, header='Content-Type', requote=True,
-                  charset=None, language=''):
+    def set_param(
+        self, param, value, header="Content-Type", requote=True, charset=None, language=""
+    ):
         """Set a parameter in the Content-Type header.
 
         If the parameter already exists in the header, its value will be
@@ -671,21 +673,19 @@ def set_param(self, param, value, header='Content-Type', requote=True,
         if not isinstance(value, tuple) and charset:
             value = (charset, language, value)
 
-        if header not in self and header.lower() == 'content-type':
-            ctype = 'text/plain'
+        if header not in self and header.lower() == "content-type":
+            ctype = "text/plain"
         else:
             ctype = self.get(header)
         if not self.get_param(param, header=header):
             if not ctype:
                 ctype = _formatparam(param, value, requote)
             else:
-                ctype = SEMISPACE.join(
-                    [ctype, _formatparam(param, value, requote)])
+                ctype = SEMISPACE.join([ctype, _formatparam(param, value, requote)])
         else:
-            ctype = ''
-            for old_param, old_value in self.get_params(header=header,
-                                                        unquote=requote):
-                append_param = ''
+            ctype = ""
+            for old_param, old_value in self.get_params(header=header, unquote=requote):
+                append_param = ""
                 if old_param.lower() == param.lower():
                     append_param = _formatparam(param, value, requote)
                 else:
@@ -698,7 +698,7 @@ def set_param(self, param, value, header='Content-Type', requote=True,
             del self[header]
             self[header] = ctype
 
-    def del_param(self, param, header='content-type', requote=True):
+    def del_param(self, param, header="content-type", requote=True):
         """Remove the given parameter completely from the Content-Type header.
 
         The header will be re-written in place without the parameter or its
@@ -708,19 +708,18 @@ def del_param(self, param, header='content-type', requote=True):
         """
         if header not in self:
             return
-        new_ctype = ''
+        new_ctype = ""
         for p, v in self.get_params(header=header, unquote=requote):
             if p.lower() != param.lower():
                 if not new_ctype:
                     new_ctype = _formatparam(p, v, requote)
                 else:
-                    new_ctype = SEMISPACE.join([new_ctype,
-                                                _formatparam(p, v, requote)])
+                    new_ctype = SEMISPACE.join([new_ctype, _formatparam(p, v, requote)])
         if new_ctype != self.get(header):
             del self[header]
             self[header] = new_ctype
 
-    def set_type(self, type, header='Content-Type', requote=True):
+    def set_type(self, type, header="Content-Type", requote=True):
         """Set the main type and subtype for the Content-Type header.
 
         type must be a string in the form "maintype/subtype", otherwise a
@@ -736,12 +735,12 @@ def set_type(self, type, header='Content-Type', requote=True):
         header.
         """
         # BAW: should we be strict?
-        if not type.count('/') == 1:
+        if not type.count("/") == 1:
             raise ValueError
         # Set the Content-Type, you get a MIME-Version
-        if header.lower() == 'content-type':
-            del self['mime-version']
-            self['MIME-Version'] = '1.0'
+        if header.lower() == "content-type":
+            del self["mime-version"]
+            self["MIME-Version"] = "1.0"
         if header not in self:
             self[header] = type
             return
@@ -761,9 +760,9 @@ def get_filename(self, failobj=None):
         `name' parameter.
         """
         missing = object()
-        filename = self.get_param('filename', missing, 'content-disposition')
+        filename = self.get_param("filename", missing, "content-disposition")
         if filename is missing:
-            filename = self.get_param('name', missing, 'content-type')
+            filename = self.get_param("name", missing, "content-type")
         if filename is missing:
             return failobj
         return utils.collapse_rfc2231_value(filename).strip()
@@ -775,7 +774,7 @@ def get_boundary(self, failobj=None):
         parameter, and it is unquoted.
         """
         missing = object()
-        boundary = self.get_param('boundary', missing)
+        boundary = self.get_param("boundary", missing)
         if boundary is missing:
             return failobj
         # RFC 2046 says that boundaries may begin but not end in w/s
@@ -792,16 +791,16 @@ def set_boundary(self, boundary):
         HeaderParseError is raised if the message has no Content-Type header.
         """
         missing = object()
-        params = self._get_params_preserve(missing, 'content-type')
+        params = self._get_params_preserve(missing, "content-type")
         if params is missing:
             # There was no Content-Type header, and we don't know what type
             # to set it to, so raise an exception.
-            raise errors.HeaderParseError('No Content-Type header found')
+            raise errors.HeaderParseError("No Content-Type header found")
         newparams = []
         foundp = False
         for pk, pv in params:
-            if pk.lower() == 'boundary':
-                newparams.append(('boundary', '"%s"' % boundary))
+            if pk.lower() == "boundary":
+                newparams.append(("boundary", '"%s"' % boundary))
                 foundp = True
             else:
                 newparams.append((pk, pv))
@@ -809,17 +808,17 @@ def set_boundary(self, boundary):
             # The original Content-Type header had no boundary attribute.
             # Tack one on the end.  BAW: should we raise an exception
             # instead???
-            newparams.append(('boundary', '"%s"' % boundary))
+            newparams.append(("boundary", '"%s"' % boundary))
         # Replace the existing Content-Type header with the new value
         newheaders = []
         for h, v in self._headers:
-            if h.lower() == 'content-type':
+            if h.lower() == "content-type":
                 parts = []
                 for k, v in newparams:
-                    if v == '':
+                    if v == "":
                         parts.append(k)
                     else:
-                        parts.append('%s=%s' % (k, v))
+                        parts.append("%s=%s" % (k, v))
                 val = SEMISPACE.join(parts)
                 newheaders.append(self.policy.header_store_parse(h, val))
 
@@ -835,23 +834,23 @@ def get_content_charset(self, failobj=None):
         failobj is returned.
         """
         missing = object()
-        charset = self.get_param('charset', missing)
+        charset = self.get_param("charset", missing)
         if charset is missing:
             return failobj
         if isinstance(charset, tuple):
             # RFC 2231 encoded, so decode it, and it better end up as ascii.
-            pcharset = charset[0] or 'us-ascii'
+            pcharset = charset[0] or "us-ascii"
             try:
                 # LookupError will be raised if the charset isn't known to
                 # Python.  UnicodeError will be raised if the encoded text
                 # contains a character not in the charset.
-                as_bytes = charset[2].encode('raw-unicode-escape')
+                as_bytes = charset[2].encode("raw-unicode-escape")
                 charset = str(as_bytes, pcharset)
             except (LookupError, UnicodeError):
                 charset = charset[2]
         # charset characters must be in us-ascii range
         try:
-            charset.encode('us-ascii')
+            charset.encode("us-ascii")
         except UnicodeError:
             return failobj
         # RFC 2046, $4.1.2 says charsets are not case sensitive
diff --git a/unix-ffi/email.message/manifest.py b/unix-ffi/email.message/manifest.py
new file mode 100644
index 000000000..d1849de35
--- /dev/null
+++ b/unix-ffi/email.message/manifest.py
@@ -0,0 +1,11 @@
+metadata(version="0.5.3")
+
+require("re")
+require("uu")
+require("base64")
+require("binascii")
+require("email.utils")
+require("email.errors")
+require("email.charset")
+
+package("email")
diff --git a/unix-ffi/email.parser/email/parser.py b/unix-ffi/email.parser/email/parser.py
new file mode 100644
index 000000000..760adeff5
--- /dev/null
+++ b/unix-ffi/email.parser/email/parser.py
@@ -0,0 +1,127 @@
+# Copyright (C) 2001-2007 Python Software Foundation
+# Author: Barry Warsaw, Thomas Wouters, Anthony Baxter
+# Contact: email-sig@python.org
+
+"""A parser of RFC 2822 and MIME email messages."""
+
+__all__ = ["Parser", "HeaderParser", "BytesParser", "BytesHeaderParser"]
+
+import warnings
+from io import StringIO, TextIOWrapper
+
+from email.feedparser import FeedParser, BytesFeedParser
+from email.message import Message
+from email._policybase import compat32
+
+
+class Parser:
+    def __init__(self, _class=Message, policy=compat32):
+        """Parser of RFC 2822 and MIME email messages.
+
+        Creates an in-memory object tree representing the email message, which
+        can then be manipulated and turned over to a Generator to return the
+        textual representation of the message.
+
+        The string must be formatted as a block of RFC 2822 headers and header
+        continuation lines, optionally preceeded by a `Unix-from' header.  The
+        header block is terminated either by the end of the string or by a
+        blank line.
+
+        _class is the class to instantiate for new message objects when they
+        must be created.  This class must have a constructor that can take
+        zero arguments.  Default is Message.Message.
+
+        The policy keyword specifies a policy object that controls a number of
+        aspects of the parser's operation.  The default policy maintains
+        backward compatibility.
+
+        """
+        self._class = _class
+        self.policy = policy
+
+    def parse(self, fp, headersonly=False):
+        """Create a message structure from the data in a file.
+
+        Reads all the data from the file and returns the root of the message
+        structure.  Optional headersonly is a flag specifying whether to stop
+        parsing after reading the headers or not.  The default is False,
+        meaning it parses the entire contents of the file.
+        """
+        feedparser = FeedParser(self._class, policy=self.policy)
+        if headersonly:
+            feedparser._set_headersonly()
+        while True:
+            data = fp.read(8192)
+            if not data:
+                break
+            feedparser.feed(data)
+        return feedparser.close()
+
+    def parsestr(self, text, headersonly=False):
+        """Create a message structure from a string.
+
+        Returns the root of the message structure.  Optional headersonly is a
+        flag specifying whether to stop parsing after reading the headers or
+        not.  The default is False, meaning it parses the entire contents of
+        the file.
+        """
+        return self.parse(StringIO(text), headersonly=headersonly)
+
+
+class HeaderParser(Parser):
+    def parse(self, fp, headersonly=True):
+        return Parser.parse(self, fp, True)
+
+    def parsestr(self, text, headersonly=True):
+        return Parser.parsestr(self, text, True)
+
+
+class BytesParser:
+    def __init__(self, *args, **kw):
+        """Parser of binary RFC 2822 and MIME email messages.
+
+        Creates an in-memory object tree representing the email message, which
+        can then be manipulated and turned over to a Generator to return the
+        textual representation of the message.
+
+        The input must be formatted as a block of RFC 2822 headers and header
+        continuation lines, optionally preceeded by a `Unix-from' header.  The
+        header block is terminated either by the end of the input or by a
+        blank line.
+
+        _class is the class to instantiate for new message objects when they
+        must be created.  This class must have a constructor that can take
+        zero arguments.  Default is Message.Message.
+        """
+        self.parser = Parser(*args, **kw)
+
+    def parse(self, fp, headersonly=False):
+        """Create a message structure from the data in a binary file.
+
+        Reads all the data from the file and returns the root of the message
+        structure.  Optional headersonly is a flag specifying whether to stop
+        parsing after reading the headers or not.  The default is False,
+        meaning it parses the entire contents of the file.
+        """
+        fp = TextIOWrapper(fp, encoding="ascii", errors="surrogateescape")
+        with fp:
+            return self.parser.parse(fp, headersonly)
+
+    def parsebytes(self, text, headersonly=False):
+        """Create a message structure from a byte string.
+
+        Returns the root of the message structure.  Optional headersonly is a
+        flag specifying whether to stop parsing after reading the headers or
+        not.  The default is False, meaning it parses the entire contents of
+        the file.
+        """
+        text = text.decode("ASCII", errors="surrogateescape")
+        return self.parser.parsestr(text, headersonly)
+
+
+class BytesHeaderParser(BytesParser):
+    def parse(self, fp, headersonly=True):
+        return BytesParser.parse(self, fp, headersonly=True)
+
+    def parsebytes(self, text, headersonly=True):
+        return BytesParser.parsebytes(self, text, headersonly=True)
diff --git a/unix-ffi/email.parser/manifest.py b/unix-ffi/email.parser/manifest.py
new file mode 100644
index 000000000..dd8aacde8
--- /dev/null
+++ b/unix-ffi/email.parser/manifest.py
@@ -0,0 +1,8 @@
+metadata(version="0.5.1")
+
+require("warnings")
+require("email.feedparser")
+require("email.message")
+require("email.internal")
+
+package("email")
diff --git a/email.utils/email/utils.py b/unix-ffi/email.utils/email/utils.py
similarity index 83%
rename from email.utils/email/utils.py
rename to unix-ffi/email.utils/email/utils.py
index 93a625c8b..b67502cff 100644
--- a/email.utils/email/utils.py
+++ b/unix-ffi/email.utils/email/utils.py
@@ -5,22 +5,22 @@
 """Miscellaneous utilities."""
 
 __all__ = [
-    'collapse_rfc2231_value',
-    'decode_params',
-    'decode_rfc2231',
-    'encode_rfc2231',
-    'formataddr',
-    'formatdate',
-    'format_datetime',
-    'getaddresses',
-    'make_msgid',
-    'mktime_tz',
-    'parseaddr',
-    'parsedate',
-    'parsedate_tz',
-    'parsedate_to_datetime',
-    'unquote',
-    ]
+    "collapse_rfc2231_value",
+    "decode_params",
+    "decode_rfc2231",
+    "encode_rfc2231",
+    "formataddr",
+    "formatdate",
+    "format_datetime",
+    "getaddresses",
+    "make_msgid",
+    "mktime_tz",
+    "parseaddr",
+    "parsedate",
+    "parsedate_tz",
+    "parsedate_to_datetime",
+    "unquote",
+]
 
 import os
 import re
@@ -45,10 +45,10 @@
 from email.encoders import _bencode, _qencode
 from email.charset import Charset
 
-COMMASPACE = ', '
-EMPTYSTRING = ''
-UEMPTYSTRING = ''
-CRLF = '\r\n'
+COMMASPACE = ", "
+EMPTYSTRING = ""
+UEMPTYSTRING = ""
+CRLF = "\r\n"
 TICK = "'"
 
 specialsre = re.compile(r'[][\\()<>@,:;".]')
@@ -56,20 +56,21 @@
 
 # How to figure out if we are processing strings that come from a byte
 # source with undecodable characters.
-_has_surrogates = re.compile(
-    '([^\ud800-\udbff]|\A)[\udc00-\udfff]([^\udc00-\udfff]|\Z)').search
+_has_surrogates = re.compile("([^\ud800-\udbff]|\A)[\udc00-\udfff]([^\udc00-\udfff]|\Z)").search
+
 
 # How to deal with a string containing bytes before handing it to the
 # application through the 'normal' interface.
 def _sanitize(string):
     # Turn any escaped bytes into unicode 'unknown' char.
-    original_bytes = string.encode('ascii', 'surrogateescape')
-    return original_bytes.decode('ascii', 'replace')
+    original_bytes = string.encode("ascii", "surrogateescape")
+    return original_bytes.decode("ascii", "replace")
 
 
 # Helpers
 
-def formataddr(pair, charset='utf-8'):
+
+def formataddr(pair, charset="utf-8"):
     """The inverse of parseaddr(), this takes a 2-tuple of the form
     (realname, email_address) and returns the string value suitable
     for an RFC 2822 From, To or Cc header.
@@ -84,25 +85,24 @@ def formataddr(pair, charset='utf-8'):
     """
     name, address = pair
     # The address MUST (per RFC) be ascii, so raise an UnicodeError if it isn't.
-    address.encode('ascii')
+    address.encode("ascii")
     if name:
         try:
-            name.encode('ascii')
+            name.encode("ascii")
         except UnicodeEncodeError:
             if isinstance(charset, str):
                 charset = Charset(charset)
             encoded_name = charset.header_encode(name)
             return "%s <%s>" % (encoded_name, address)
         else:
-            quotes = ''
+            quotes = ""
             if specialsre.search(name):
                 quotes = '"'
-            name = escapesre.sub(r'\\\g<0>', name)
-            return '%s%s%s <%s>' % (quotes, name, quotes, address)
+            name = escapesre.sub(r"\\\g<0>", name)
+            return "%s%s%s <%s>" % (quotes, name, quotes, address)
     return address
 
 
-
 def getaddresses(fieldvalues):
     """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
     all = COMMASPACE.join(fieldvalues)
@@ -110,8 +110,8 @@ def getaddresses(fieldvalues):
     return a.addresslist
 
 
-
-ecre = re.compile(r'''
+ecre = re.compile(
+    r"""
   =\?                   # literal =?
   (?P<charset>[^?]*?)   # non-greedy up to the next ? is the charset
   \?                    # literal ?
@@ -119,17 +119,25 @@ def getaddresses(fieldvalues):
   \?                    # literal ?
   (?P<atom>.*?)         # non-greedy up to the next ?= is the atom
   \?=                   # literal ?=
-  ''', re.VERBOSE | re.IGNORECASE)
+  """,
+    re.VERBOSE | re.IGNORECASE,
+)
 
 
 def _format_timetuple_and_zone(timetuple, zone):
-    return '%s, %02d %s %04d %02d:%02d:%02d %s' % (
-        ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]],
+    return "%s, %02d %s %04d %02d:%02d:%02d %s" % (
+        ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][timetuple[6]],
         timetuple[2],
-        ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
-         'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1],
-        timetuple[0], timetuple[3], timetuple[4], timetuple[5],
-        zone)
+        ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][
+            timetuple[1] - 1
+        ],
+        timetuple[0],
+        timetuple[3],
+        timetuple[4],
+        timetuple[5],
+        zone,
+    )
+
 
 def formatdate(timeval=None, localtime=False, usegmt=False):
     """Returns a date string as specified by RFC 2822, e.g.:
@@ -163,19 +171,20 @@ def formatdate(timeval=None, localtime=False, usegmt=False):
         # Remember offset is in seconds west of UTC, but the timezone is in
         # minutes east of UTC, so the signs differ.
         if offset > 0:
-            sign = '-'
+            sign = "-"
         else:
-            sign = '+'
-        zone = '%s%02d%02d' % (sign, hours, minutes // 60)
+            sign = "+"
+        zone = "%s%02d%02d" % (sign, hours, minutes // 60)
     else:
         now = time.gmtime(timeval)
         # Timezone offset is always -0000
         if usegmt:
-            zone = 'GMT'
+            zone = "GMT"
         else:
-            zone = '-0000'
+            zone = "-0000"
     return _format_timetuple_and_zone(now, zone)
 
+
 def format_datetime(dt, usegmt=False):
     """Turn a datetime into a date string as specified in RFC 2822.
 
@@ -187,9 +196,9 @@ def format_datetime(dt, usegmt=False):
     if usegmt:
         if dt.tzinfo is None or dt.tzinfo != datetime.timezone.utc:
             raise ValueError("usegmt option requires a UTC datetime")
-        zone = 'GMT'
+        zone = "GMT"
     elif dt.tzinfo is None:
-        zone = '-0000'
+        zone = "-0000"
     else:
         zone = dt.strftime("%z")
     return _format_timetuple_and_zone(now, zone)
@@ -206,16 +215,19 @@ def make_msgid(idstring=None, domain=None):
     defined hostname.
     """
     timeval = time.time()
-    utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
-    pid = os.getpid()
+    utcdate = time.strftime("%Y%m%d%H%M%S", time.gmtime(timeval))
+    if hasattr(os, "getpid"):
+        pid = os.getpid()
+    else:
+        pid = 0
     randint = random.randrange(100000)
     if idstring is None:
-        idstring = ''
+        idstring = ""
     else:
-        idstring = '.' + idstring
+        idstring = "." + idstring
     if domain is None:
         domain = socket.getfqdn()
-    msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, domain)
+    msgid = "<%s.%s.%s%s@%s>" % (utcdate, pid, randint, idstring, domain)
     return msgid
 
 
@@ -223,14 +235,13 @@ def parsedate_to_datetime(data):
     *dtuple, tz = _parsedate_tz(data)
     if tz is None:
         return datetime.datetime(*dtuple[:6])
-    return datetime.datetime(*dtuple[:6],
-            tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
+    return datetime.datetime(*dtuple[:6], tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
 
 
 def parseaddr(addr):
     addrs = _AddressList(addr).addresslist
     if not addrs:
-        return '', ''
+        return "", ""
     return addrs[0]
 
 
@@ -239,13 +250,12 @@ def unquote(str):
     """Remove quotes from a string."""
     if len(str) > 1:
         if str.startswith('"') and str.endswith('"'):
-            return str[1:-1].replace('\\\\', '\\').replace('\\"', '"')
-        if str.startswith('<') and str.endswith('>'):
+            return str[1:-1].replace("\\\\", "\\").replace('\\"', '"')
+        if str.startswith("<") and str.endswith(">"):
             return str[1:-1]
     return str
 
 
-
 # RFC2231-related functions - parameter encoding and decoding
 def decode_rfc2231(s):
     """Decode string according to RFC 2231"""
@@ -262,16 +272,16 @@ def encode_rfc2231(s, charset=None, language=None):
     charset is given but not language, the string is encoded using the empty
     string for language.
     """
-    s = urllib.parse.quote(s, safe='', encoding=charset or 'ascii')
+    s = urllib.parse.quote(s, safe="", encoding=charset or "ascii")
     if charset is None and language is None:
         return s
     if language is None:
-        language = ''
+        language = ""
     return "%s'%s'%s" % (charset, language, s)
 
 
-rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$',
-    re.ASCII)
+rfc2231_continuation = re.compile(r"^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$", re.ASCII)
+
 
 def decode_params(params):
     """Decode parameters list according to RFC 2231.
@@ -289,14 +299,14 @@ def decode_params(params):
     new_params.append((name, value))
     while params:
         name, value = params.pop(0)
-        if name.endswith('*'):
+        if name.endswith("*"):
             encoded = True
         else:
             encoded = False
         value = unquote(value)
         mo = rfc2231_continuation.match(name)
         if mo:
-            name, num = mo.group('name', 'num')
+            name, num = mo.group("name", "num")
             if num is not None:
                 num = int(num)
             rfc2231_params.setdefault(name, []).append((num, value, encoded))
@@ -329,15 +339,15 @@ def decode_params(params):
                 new_params.append((name, '"%s"' % value))
     return new_params
 
-def collapse_rfc2231_value(value, errors='replace',
-                           fallback_charset='us-ascii'):
+
+def collapse_rfc2231_value(value, errors="replace", fallback_charset="us-ascii"):
     if not isinstance(value, tuple) or len(value) != 3:
         return unquote(value)
     # While value comes to us as a unicode string, we need it to be a bytes
     # object.  We do not want bytes() normal utf-8 decoder, we want a straight
     # interpretation of the string as character bytes.
     charset, language, text = value
-    rawbytes = bytes(text, 'raw-unicode-escape')
+    rawbytes = bytes(text, "raw-unicode-escape")
     try:
         return str(rawbytes, charset, errors)
     except LookupError:
@@ -351,6 +361,7 @@ def collapse_rfc2231_value(value, errors='replace',
 # better than not having it.
 #
 
+
 def localtime(dt=None, isdst=-1):
     """Return local time as an aware datetime object.
 
diff --git a/unix-ffi/email.utils/manifest.py b/unix-ffi/email.utils/manifest.py
new file mode 100644
index 000000000..a7208536d
--- /dev/null
+++ b/unix-ffi/email.utils/manifest.py
@@ -0,0 +1,13 @@
+metadata(version="3.3.4")
+
+require("os")
+require("re")
+require("base64")
+require("random")
+require("datetime")
+require("urllib.parse")
+require("warnings")
+require("quopri")
+require("email.charset")
+
+package("email")
diff --git a/fcntl/fcntl.py b/unix-ffi/fcntl/fcntl.py
similarity index 100%
rename from fcntl/fcntl.py
rename to unix-ffi/fcntl/fcntl.py
diff --git a/unix-ffi/fcntl/manifest.py b/unix-ffi/fcntl/manifest.py
new file mode 100644
index 000000000..e572a58e8
--- /dev/null
+++ b/unix-ffi/fcntl/manifest.py
@@ -0,0 +1,7 @@
+metadata(version="0.0.4")
+
+# Originally written by Paul Sokolovsky.
+
+require("ffilib")
+
+module("fcntl.py")
diff --git a/ffilib/ffilib.py b/unix-ffi/ffilib/ffilib.py
similarity index 83%
rename from ffilib/ffilib.py
rename to unix-ffi/ffilib/ffilib.py
index dc4d672a2..e79448fea 100644
--- a/ffilib/ffilib.py
+++ b/unix-ffi/ffilib/ffilib.py
@@ -1,4 +1,5 @@
 import sys
+
 try:
     import ffi
 except ImportError:
@@ -6,6 +7,7 @@
 
 _cache = {}
 
+
 def open(name, maxver=10, extra=()):
     if not ffi:
         return None
@@ -13,16 +15,18 @@ def open(name, maxver=10, extra=()):
         return _cache[name]
     except KeyError:
         pass
+
     def libs():
         if sys.platform == "linux":
-            yield '%s.so' % name
+            yield "%s.so" % name
             for i in range(maxver, -1, -1):
-                yield '%s.so.%u' % (name, i)
+                yield "%s.so.%u" % (name, i)
         else:
-            for ext in ('dylib', 'dll'):
-                yield '%s.%s' % (name, ext)
+            for ext in ("dylib", "dll"):
+                yield "%s.%s" % (name, ext)
         for n in extra:
             yield n
+
     err = None
     for n in libs():
         try:
@@ -33,9 +37,11 @@ def libs():
             err = e
     raise err
 
+
 def libc():
     return open("libc", 6)
 
+
 # Find out bitness of the platform, even if long ints are not supported
 # TODO: All bitness differences should be removed from micropython-lib, and
 # this snippet too.
diff --git a/unix-ffi/ffilib/manifest.py b/unix-ffi/ffilib/manifest.py
new file mode 100644
index 000000000..fe795a2ec
--- /dev/null
+++ b/unix-ffi/ffilib/manifest.py
@@ -0,0 +1,8 @@
+metadata(
+    description="MicroPython FFI helper module to easily interface with underlying shared libraries",
+    version="0.1.3",
+)
+
+# Originally written by Damien George.
+
+module("ffilib.py")
diff --git a/getopt/getopt.py b/unix-ffi/getopt/getopt.py
similarity index 79%
rename from getopt/getopt.py
rename to unix-ffi/getopt/getopt.py
index 3d6ecbddb..dd8d70538 100644
--- a/getopt/getopt.py
+++ b/unix-ffi/getopt/getopt.py
@@ -31,19 +31,23 @@
 # - a option string with a W followed by semicolon should
 #   treat "-W foo" as "--foo"
 
-__all__ = ["GetoptError","error","getopt","gnu_getopt"]
+__all__ = ["GetoptError", "error", "getopt", "gnu_getopt"]
 
 import os
+
 try:
     from gettext import gettext as _
 except ImportError:
     # Bootstrapping Python: gettext's dependencies not built yet
-    def _(s): return s
+    def _(s):
+        return s
+
 
 class GetoptError(Exception):
-    opt = ''
-    msg = ''
-    def __init__(self, msg, opt=''):
+    opt = ""
+    msg = ""
+
+    def __init__(self, msg, opt=""):
         self.msg = msg
         self.opt = opt
         Exception.__init__(self, msg, opt)
@@ -51,9 +55,11 @@ def __init__(self, msg, opt=''):
     def __str__(self):
         return self.msg
 
-error = GetoptError # backward compatibility
 
-def getopt(args, shortopts, longopts = []):
+error = GetoptError  # backward compatibility
+
+
+def getopt(args, shortopts, longopts=[]):
     """getopt(args, options[, long_options]) -> opts, args
 
     Parses command line options and parameter list.  args is the
@@ -85,18 +91,19 @@ def getopt(args, shortopts, longopts = []):
         longopts = [longopts]
     else:
         longopts = list(longopts)
-    while args and args[0].startswith('-') and args[0] != '-':
-        if args[0] == '--':
+    while args and args[0].startswith("-") and args[0] != "-":
+        if args[0] == "--":
             args = args[1:]
             break
-        if args[0].startswith('--'):
+        if args[0].startswith("--"):
             opts, args = do_longs(opts, args[0][2:], longopts, args[1:])
         else:
             opts, args = do_shorts(opts, args[0][1:], shortopts, args[1:])
 
     return opts, args
 
-def gnu_getopt(args, shortopts, longopts = []):
+
+def gnu_getopt(args, shortopts, longopts=[]):
     """getopt(args, options[, long_options]) -> opts, args
 
     This function works like getopt(), except that GNU style scanning
@@ -119,7 +126,7 @@ def gnu_getopt(args, shortopts, longopts = []):
         longopts = list(longopts)
 
     # Allow options after non-option arguments?
-    if shortopts.startswith('+'):
+    if shortopts.startswith("+"):
         shortopts = shortopts[1:]
         all_options_first = True
     elif os.environ.get("POSIXLY_CORRECT"):
@@ -128,13 +135,13 @@ def gnu_getopt(args, shortopts, longopts = []):
         all_options_first = False
 
     while args:
-        if args[0] == '--':
+        if args[0] == "--":
             prog_args += args[1:]
             break
 
-        if args[0][:2] == '--':
+        if args[0][:2] == "--":
             opts, args = do_longs(opts, args[0][2:], longopts, args[1:])
-        elif args[0][:1] == '-' and args[0] != '-':
+        elif args[0][:1] == "-" and args[0] != "-":
             opts, args = do_shorts(opts, args[0][1:], shortopts, args[1:])
         else:
             if all_options_first:
@@ -146,70 +153,75 @@ def gnu_getopt(args, shortopts, longopts = []):
 
     return opts, prog_args
 
+
 def do_longs(opts, opt, longopts, args):
     try:
-        i = opt.index('=')
+        i = opt.index("=")
     except ValueError:
         optarg = None
     else:
-        opt, optarg = opt[:i], opt[i+1:]
+        opt, optarg = opt[:i], opt[i + 1 :]
 
     has_arg, opt = long_has_args(opt, longopts)
     if has_arg:
         if optarg is None:
             if not args:
-                raise GetoptError(_('option --%s requires argument') % opt, opt)
+                raise GetoptError(_("option --%s requires argument") % opt, opt)
             optarg, args = args[0], args[1:]
     elif optarg is not None:
-        raise GetoptError(_('option --%s must not have an argument') % opt, opt)
-    opts.append(('--' + opt, optarg or ''))
+        raise GetoptError(_("option --%s must not have an argument") % opt, opt)
+    opts.append(("--" + opt, optarg or ""))
     return opts, args
 
+
 # Return:
 #   has_arg?
 #   full option name
 def long_has_args(opt, longopts):
     possibilities = [o for o in longopts if o.startswith(opt)]
     if not possibilities:
-        raise GetoptError(_('option --%s not recognized') % opt, opt)
+        raise GetoptError(_("option --%s not recognized") % opt, opt)
     # Is there an exact match?
     if opt in possibilities:
         return False, opt
-    elif opt + '=' in possibilities:
+    elif opt + "=" in possibilities:
         return True, opt
     # No exact match, so better be unique.
     if len(possibilities) > 1:
         # XXX since possibilities contains all valid continuations, might be
         # nice to work them into the error msg
-        raise GetoptError(_('option --%s not a unique prefix') % opt, opt)
+        raise GetoptError(_("option --%s not a unique prefix") % opt, opt)
     assert len(possibilities) == 1
     unique_match = possibilities[0]
-    has_arg = unique_match.endswith('=')
+    has_arg = unique_match.endswith("=")
     if has_arg:
         unique_match = unique_match[:-1]
     return has_arg, unique_match
 
+
 def do_shorts(opts, optstring, shortopts, args):
-    while optstring != '':
+    while optstring != "":
         opt, optstring = optstring[0], optstring[1:]
         if short_has_arg(opt, shortopts):
-            if optstring == '':
+            if optstring == "":
                 if not args:
-                    raise GetoptError(_('option -%s requires argument') % opt,
-                                      opt)
+                    raise GetoptError(_("option -%s requires argument") % opt, opt)
                 optstring, args = args[0], args[1:]
-            optarg, optstring = optstring, ''
+            optarg, optstring = optstring, ""
         else:
-            optarg = ''
-        opts.append(('-' + opt, optarg))
+            optarg = ""
+        opts.append(("-" + opt, optarg))
     return opts, args
 
+
 def short_has_arg(opt, shortopts):
     for i in range(len(shortopts)):
-        if opt == shortopts[i] != ':':
-            return shortopts.startswith(':', i+1)
-    raise GetoptError(_('option -%s not recognized') % opt, opt)
+        if opt == shortopts[i] != ":":
+            return shortopts.startswith(":", i + 1)
+    raise GetoptError(_("option -%s not recognized") % opt, opt)
+
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     import sys
+
     print(getopt(sys.argv[1:], "a:b", ["alpha=", "beta"]))
diff --git a/unix-ffi/getopt/manifest.py b/unix-ffi/getopt/manifest.py
new file mode 100644
index 000000000..cde6c4c09
--- /dev/null
+++ b/unix-ffi/getopt/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="3.3.4")
+
+require("os")
+
+module("getopt.py")
diff --git a/unix-ffi/gettext/gettext.py b/unix-ffi/gettext/gettext.py
new file mode 100644
index 000000000..5e02ae6b1
--- /dev/null
+++ b/unix-ffi/gettext/gettext.py
@@ -0,0 +1,14 @@
+import ffilib
+
+libc = ffilib.libc()
+
+gettext_ = libc.func("s", "gettext", "s")
+ngettext_ = libc.func("s", "ngettext", "ssL")
+
+
+def gettext(message):
+    return gettext_(message)
+
+
+def ngettext(singular, plural, n):
+    return ngettext_(singular, plural, n)
diff --git a/unix-ffi/gettext/manifest.py b/unix-ffi/gettext/manifest.py
new file mode 100644
index 000000000..fe40b01b6
--- /dev/null
+++ b/unix-ffi/gettext/manifest.py
@@ -0,0 +1,7 @@
+metadata(version="0.1.0")
+
+# Originally written by Riccardo Magliocchetti.
+
+require("ffilib")
+
+module("gettext.py")
diff --git a/unix-ffi/gettext/test_gettext.py b/unix-ffi/gettext/test_gettext.py
new file mode 100644
index 000000000..263aa314a
--- /dev/null
+++ b/unix-ffi/gettext/test_gettext.py
@@ -0,0 +1,16 @@
+import gettext
+
+msg = gettext.gettext("yes")
+assert msg == "yes"
+
+msg = gettext.ngettext("one", "two", 1)
+assert msg == "one"
+
+msg = gettext.ngettext("one", "two", 2)
+assert msg == "two"
+
+msg = gettext.ngettext("one", "two", 0)
+assert msg == "two"
+
+msg = gettext.ngettext("one", "two", "three")
+assert msg == "two"
diff --git a/glob/glob.py b/unix-ffi/glob/glob.py
similarity index 94%
rename from glob/glob.py
rename to unix-ffi/glob/glob.py
index 3baf745a1..e32e9decc 100644
--- a/glob/glob.py
+++ b/unix-ffi/glob/glob.py
@@ -7,6 +7,7 @@
 
 __all__ = ["glob", "iglob"]
 
+
 def glob(pathname):
     """Return a list of paths matching a pathname pattern.
 
@@ -18,6 +19,7 @@ def glob(pathname):
     """
     return list(iglob(pathname))
 
+
 def iglob(pathname):
     """Return an iterator which yields the paths matching a pathname pattern.
 
@@ -51,14 +53,16 @@ def iglob(pathname):
         for name in glob_in_dir(dirname, basename):
             yield os.path.join(dirname, name)
 
+
 # These 2 helper functions non-recursively glob inside a literal directory.
 # They return a list of basenames. `glob1` accepts a pattern while `glob0`
 # takes a literal basename (so it only has to check for its existence).
 
+
 def glob1(dirname, pattern):
     if not dirname:
         if isinstance(pattern, bytes):
-            dirname = bytes(os.curdir, 'ASCII')
+            dirname = bytes(os.curdir, "ASCII")
         else:
             dirname = os.curdir
     try:
@@ -69,6 +73,7 @@ def glob1(dirname, pattern):
         names = [x for x in names if not _ishidden(x)]
     return fnmatch.filter(names, pattern)
 
+
 def glob0(dirname, basename):
     if not basename:
         # `os.path.split()` returns an empty basename for paths ending with a
@@ -81,8 +86,9 @@ def glob0(dirname, basename):
     return []
 
 
-magic_check = re.compile('[*?[]')
-magic_check_bytes = re.compile(b'[*?[]')
+magic_check = re.compile("[*?[]")
+magic_check_bytes = re.compile(b"[*?[]")
+
 
 def has_magic(s):
     if isinstance(s, bytes):
@@ -91,5 +97,6 @@ def has_magic(s):
         match = magic_check.search(s)
     return match is not None
 
+
 def _ishidden(path):
-    return path[0] in ('.', b'.'[0])
+    return path[0] in (".", b"."[0])
diff --git a/unix-ffi/glob/manifest.py b/unix-ffi/glob/manifest.py
new file mode 100644
index 000000000..2d2fab31c
--- /dev/null
+++ b/unix-ffi/glob/manifest.py
@@ -0,0 +1,8 @@
+metadata(version="0.5.2")
+
+require("os")
+require("os-path")
+require("re")
+require("fnmatch")
+
+module("glob.py")
diff --git a/unix-ffi/glob/test_glob.py b/unix-ffi/glob/test_glob.py
new file mode 100644
index 000000000..dd53e5f14
--- /dev/null
+++ b/unix-ffi/glob/test_glob.py
@@ -0,0 +1,183 @@
+import glob
+import os
+import os.path
+import shutil
+import sys
+import unittest
+
+from test.support import run_unittest, TESTFN, skip_unless_symlink, can_symlink, create_empty_file
+
+
+class GlobTests(unittest.TestCase):
+    def norm(self, *parts):
+        return os.path.normpath(os.path.join(self.tempdir, *parts))
+
+    def mktemp(self, *parts):
+        filename = self.norm(*parts)
+        base, file = os.path.split(filename)
+        if not os.path.exists(base):
+            os.makedirs(base)
+        create_empty_file(filename)
+
+    def setUp(self):
+        self.tempdir = TESTFN + "_dir"
+        self.mktemp("a", "D")
+        self.mktemp("aab", "F")
+        self.mktemp(".aa", "G")
+        self.mktemp(".bb", "H")
+        self.mktemp("aaa", "zzzF")
+        self.mktemp("ZZZ")
+        self.mktemp("a", "bcd", "EF")
+        self.mktemp("a", "bcd", "efg", "ha")
+        if can_symlink():
+            os.symlink(self.norm("broken"), self.norm("sym1"))
+            os.symlink("broken", self.norm("sym2"))
+            os.symlink(os.path.join("a", "bcd"), self.norm("sym3"))
+
+    def tearDown(self):
+        shutil.rmtree(self.tempdir)
+
+    def glob(self, *parts):
+        if len(parts) == 1:
+            pattern = parts[0]
+        else:
+            pattern = os.path.join(*parts)
+        p = os.path.join(self.tempdir, pattern)
+        res = glob.glob(p)
+        self.assertEqual(list(glob.iglob(p)), res)
+        bres = [os.fsencode(x) for x in res]
+        # Bytes globbing is not support on MicroPython
+        #        self.assertEqual(glob.glob(os.fsencode(p)), bres)
+        #        self.assertEqual(list(glob.iglob(os.fsencode(p))), bres)
+        return res
+
+    def assertSequencesEqual_noorder(self, l1, l2):
+        l1 = list(l1)
+        l2 = list(l2)
+        self.assertEqual(set(l1), set(l2))
+        self.assertEqual(sorted(l1), sorted(l2))
+
+    def test_glob_literal(self):
+        eq = self.assertSequencesEqual_noorder
+        eq(self.glob("a"), [self.norm("a")])
+        eq(self.glob("a", "D"), [self.norm("a", "D")])
+        eq(self.glob("aab"), [self.norm("aab")])
+        eq(self.glob("zymurgy"), [])
+
+        res = glob.glob("*")
+        self.assertEqual({type(r) for r in res}, {str})
+        res = glob.glob(os.path.join(os.curdir, "*"))
+        self.assertEqual({type(r) for r in res}, {str})
+
+    #        res = glob.glob(b'*')
+    #        self.assertEqual({type(r) for r in res}, {bytes})
+    #        res = glob.glob(os.path.join(os.fsencode(os.curdir), b'*'))
+    #        self.assertEqual({type(r) for r in res}, {bytes})
+
+    def test_glob_one_directory(self):
+        eq = self.assertSequencesEqual_noorder
+        eq(self.glob("a*"), map(self.norm, ["a", "aab", "aaa"]))
+        eq(self.glob("*a"), map(self.norm, ["a", "aaa"]))
+        eq(self.glob(".*"), map(self.norm, [".aa", ".bb"]))
+        eq(self.glob("?aa"), map(self.norm, ["aaa"]))
+        eq(self.glob("aa?"), map(self.norm, ["aaa", "aab"]))
+        eq(self.glob("aa[ab]"), map(self.norm, ["aaa", "aab"]))
+        eq(self.glob("*q"), [])
+
+    def test_glob_nested_directory(self):
+        eq = self.assertSequencesEqual_noorder
+        if os.path.normcase("abCD") == "abCD":
+            # case-sensitive filesystem
+            eq(self.glob("a", "bcd", "E*"), [self.norm("a", "bcd", "EF")])
+        else:
+            # case insensitive filesystem
+            eq(
+                self.glob("a", "bcd", "E*"),
+                [self.norm("a", "bcd", "EF"), self.norm("a", "bcd", "efg")],
+            )
+        eq(self.glob("a", "bcd", "*g"), [self.norm("a", "bcd", "efg")])
+
+    def test_glob_directory_names(self):
+        eq = self.assertSequencesEqual_noorder
+        eq(self.glob("*", "D"), [self.norm("a", "D")])
+        eq(self.glob("*", "*a"), [])
+        eq(self.glob("a", "*", "*", "*a"), [self.norm("a", "bcd", "efg", "ha")])
+        eq(self.glob("?a?", "*F"), [self.norm("aaa", "zzzF"), self.norm("aab", "F")])
+
+    def test_glob_directory_with_trailing_slash(self):
+        # Patterns ending with a slash shouldn't match non-dirs
+        res = glob.glob(self.norm("Z*Z") + os.sep)
+        self.assertEqual(res, [])
+        res = glob.glob(self.norm("ZZZ") + os.sep)
+        self.assertEqual(res, [])
+        # When there is a wildcard pattern which ends with os.sep, glob()
+        # doesn't blow up.
+        res = glob.glob(self.norm("aa*") + os.sep)
+        self.assertEqual(len(res), 2)
+        # either of these results is reasonable
+        self.assertIn(
+            set(res),
+            [
+                {self.norm("aaa"), self.norm("aab")},
+                {self.norm("aaa") + os.sep, self.norm("aab") + os.sep},
+            ],
+        )
+
+    @unittest.skip("unsupported on MicroPython")
+    def test_glob_bytes_directory_with_trailing_slash(self):
+        # Same as test_glob_directory_with_trailing_slash, but with a
+        # bytes argument.
+        res = glob.glob(os.fsencode(self.norm("Z*Z") + os.sep))
+        self.assertEqual(res, [])
+        res = glob.glob(os.fsencode(self.norm("ZZZ") + os.sep))
+        self.assertEqual(res, [])
+        res = glob.glob(os.fsencode(self.norm("aa*") + os.sep))
+        self.assertEqual(len(res), 2)
+        # either of these results is reasonable
+        self.assertIn(
+            set(res),
+            [
+                {os.fsencode(self.norm("aaa")), os.fsencode(self.norm("aab"))},
+                {os.fsencode(self.norm("aaa") + os.sep), os.fsencode(self.norm("aab") + os.sep)},
+            ],
+        )
+
+    @skip_unless_symlink
+    def test_glob_symlinks(self):
+        eq = self.assertSequencesEqual_noorder
+        eq(self.glob("sym3"), [self.norm("sym3")])
+        eq(self.glob("sym3", "*"), [self.norm("sym3", "EF"), self.norm("sym3", "efg")])
+        self.assertIn(
+            self.glob("sym3" + os.sep), [[self.norm("sym3")], [self.norm("sym3") + os.sep]]
+        )
+        eq(
+            self.glob("*", "*F"),
+            [self.norm("aaa", "zzzF"), self.norm("aab", "F"), self.norm("sym3", "EF")],
+        )
+
+    @skip_unless_symlink
+    def test_glob_broken_symlinks(self):
+        eq = self.assertSequencesEqual_noorder
+        eq(self.glob("sym*"), [self.norm("sym1"), self.norm("sym2"), self.norm("sym3")])
+        eq(self.glob("sym1"), [self.norm("sym1")])
+        eq(self.glob("sym2"), [self.norm("sym2")])
+
+    @unittest.skipUnless(sys.platform == "win32", "Win32 specific test")
+    def test_glob_magic_in_drive(self):
+        eq = self.assertSequencesEqual_noorder
+        eq(glob.glob("*:"), [])
+        eq(glob.glob(b"*:"), [])
+        eq(glob.glob("?:"), [])
+        eq(glob.glob(b"?:"), [])
+        eq(glob.glob("\\\\?\\c:\\"), ["\\\\?\\c:\\"])
+        eq(glob.glob(b"\\\\?\\c:\\"), [b"\\\\?\\c:\\"])
+        eq(glob.glob("\\\\*\\*\\"), [])
+        eq(glob.glob(b"\\\\*\\*\\"), [])
+
+
+def test_main():
+    run_unittest(GlobTests)
+
+
+if __name__ == "__main__":
+    test_main()
diff --git a/unix-ffi/html.entities/html/entities.py b/unix-ffi/html.entities/html/entities.py
new file mode 100644
index 000000000..223af74aa
--- /dev/null
+++ b/unix-ffi/html.entities/html/entities.py
@@ -0,0 +1,2506 @@
+"""HTML character entity references."""
+
+# maps the HTML entity name to the Unicode codepoint
+name2codepoint = {
+    "AElig": 0x00C6,  # latin capital letter AE = latin capital ligature AE, U+00C6 ISOlat1
+    "Aacute": 0x00C1,  # latin capital letter A with acute, U+00C1 ISOlat1
+    "Acirc": 0x00C2,  # latin capital letter A with circumflex, U+00C2 ISOlat1
+    "Agrave": 0x00C0,  # latin capital letter A with grave = latin capital letter A grave, U+00C0 ISOlat1
+    "Alpha": 0x0391,  # greek capital letter alpha, U+0391
+    "Aring": 0x00C5,  # latin capital letter A with ring above = latin capital letter A ring, U+00C5 ISOlat1
+    "Atilde": 0x00C3,  # latin capital letter A with tilde, U+00C3 ISOlat1
+    "Auml": 0x00C4,  # latin capital letter A with diaeresis, U+00C4 ISOlat1
+    "Beta": 0x0392,  # greek capital letter beta, U+0392
+    "Ccedil": 0x00C7,  # latin capital letter C with cedilla, U+00C7 ISOlat1
+    "Chi": 0x03A7,  # greek capital letter chi, U+03A7
+    "Dagger": 0x2021,  # double dagger, U+2021 ISOpub
+    "Delta": 0x0394,  # greek capital letter delta, U+0394 ISOgrk3
+    "ETH": 0x00D0,  # latin capital letter ETH, U+00D0 ISOlat1
+    "Eacute": 0x00C9,  # latin capital letter E with acute, U+00C9 ISOlat1
+    "Ecirc": 0x00CA,  # latin capital letter E with circumflex, U+00CA ISOlat1
+    "Egrave": 0x00C8,  # latin capital letter E with grave, U+00C8 ISOlat1
+    "Epsilon": 0x0395,  # greek capital letter epsilon, U+0395
+    "Eta": 0x0397,  # greek capital letter eta, U+0397
+    "Euml": 0x00CB,  # latin capital letter E with diaeresis, U+00CB ISOlat1
+    "Gamma": 0x0393,  # greek capital letter gamma, U+0393 ISOgrk3
+    "Iacute": 0x00CD,  # latin capital letter I with acute, U+00CD ISOlat1
+    "Icirc": 0x00CE,  # latin capital letter I with circumflex, U+00CE ISOlat1
+    "Igrave": 0x00CC,  # latin capital letter I with grave, U+00CC ISOlat1
+    "Iota": 0x0399,  # greek capital letter iota, U+0399
+    "Iuml": 0x00CF,  # latin capital letter I with diaeresis, U+00CF ISOlat1
+    "Kappa": 0x039A,  # greek capital letter kappa, U+039A
+    "Lambda": 0x039B,  # greek capital letter lambda, U+039B ISOgrk3
+    "Mu": 0x039C,  # greek capital letter mu, U+039C
+    "Ntilde": 0x00D1,  # latin capital letter N with tilde, U+00D1 ISOlat1
+    "Nu": 0x039D,  # greek capital letter nu, U+039D
+    "OElig": 0x0152,  # latin capital ligature OE, U+0152 ISOlat2
+    "Oacute": 0x00D3,  # latin capital letter O with acute, U+00D3 ISOlat1
+    "Ocirc": 0x00D4,  # latin capital letter O with circumflex, U+00D4 ISOlat1
+    "Ograve": 0x00D2,  # latin capital letter O with grave, U+00D2 ISOlat1
+    "Omega": 0x03A9,  # greek capital letter omega, U+03A9 ISOgrk3
+    "Omicron": 0x039F,  # greek capital letter omicron, U+039F
+    "Oslash": 0x00D8,  # latin capital letter O with stroke = latin capital letter O slash, U+00D8 ISOlat1
+    "Otilde": 0x00D5,  # latin capital letter O with tilde, U+00D5 ISOlat1
+    "Ouml": 0x00D6,  # latin capital letter O with diaeresis, U+00D6 ISOlat1
+    "Phi": 0x03A6,  # greek capital letter phi, U+03A6 ISOgrk3
+    "Pi": 0x03A0,  # greek capital letter pi, U+03A0 ISOgrk3
+    "Prime": 0x2033,  # double prime = seconds = inches, U+2033 ISOtech
+    "Psi": 0x03A8,  # greek capital letter psi, U+03A8 ISOgrk3
+    "Rho": 0x03A1,  # greek capital letter rho, U+03A1
+    "Scaron": 0x0160,  # latin capital letter S with caron, U+0160 ISOlat2
+    "Sigma": 0x03A3,  # greek capital letter sigma, U+03A3 ISOgrk3
+    "THORN": 0x00DE,  # latin capital letter THORN, U+00DE ISOlat1
+    "Tau": 0x03A4,  # greek capital letter tau, U+03A4
+    "Theta": 0x0398,  # greek capital letter theta, U+0398 ISOgrk3
+    "Uacute": 0x00DA,  # latin capital letter U with acute, U+00DA ISOlat1
+    "Ucirc": 0x00DB,  # latin capital letter U with circumflex, U+00DB ISOlat1
+    "Ugrave": 0x00D9,  # latin capital letter U with grave, U+00D9 ISOlat1
+    "Upsilon": 0x03A5,  # greek capital letter upsilon, U+03A5 ISOgrk3
+    "Uuml": 0x00DC,  # latin capital letter U with diaeresis, U+00DC ISOlat1
+    "Xi": 0x039E,  # greek capital letter xi, U+039E ISOgrk3
+    "Yacute": 0x00DD,  # latin capital letter Y with acute, U+00DD ISOlat1
+    "Yuml": 0x0178,  # latin capital letter Y with diaeresis, U+0178 ISOlat2
+    "Zeta": 0x0396,  # greek capital letter zeta, U+0396
+    "aacute": 0x00E1,  # latin small letter a with acute, U+00E1 ISOlat1
+    "acirc": 0x00E2,  # latin small letter a with circumflex, U+00E2 ISOlat1
+    "acute": 0x00B4,  # acute accent = spacing acute, U+00B4 ISOdia
+    "aelig": 0x00E6,  # latin small letter ae = latin small ligature ae, U+00E6 ISOlat1
+    "agrave": 0x00E0,  # latin small letter a with grave = latin small letter a grave, U+00E0 ISOlat1
+    "alefsym": 0x2135,  # alef symbol = first transfinite cardinal, U+2135 NEW
+    "alpha": 0x03B1,  # greek small letter alpha, U+03B1 ISOgrk3
+    "amp": 0x0026,  # ampersand, U+0026 ISOnum
+    "and": 0x2227,  # logical and = wedge, U+2227 ISOtech
+    "ang": 0x2220,  # angle, U+2220 ISOamso
+    "aring": 0x00E5,  # latin small letter a with ring above = latin small letter a ring, U+00E5 ISOlat1
+    "asymp": 0x2248,  # almost equal to = asymptotic to, U+2248 ISOamsr
+    "atilde": 0x00E3,  # latin small letter a with tilde, U+00E3 ISOlat1
+    "auml": 0x00E4,  # latin small letter a with diaeresis, U+00E4 ISOlat1
+    "bdquo": 0x201E,  # double low-9 quotation mark, U+201E NEW
+    "beta": 0x03B2,  # greek small letter beta, U+03B2 ISOgrk3
+    "brvbar": 0x00A6,  # broken bar = broken vertical bar, U+00A6 ISOnum
+    "bull": 0x2022,  # bullet = black small circle, U+2022 ISOpub
+    "cap": 0x2229,  # intersection = cap, U+2229 ISOtech
+    "ccedil": 0x00E7,  # latin small letter c with cedilla, U+00E7 ISOlat1
+    "cedil": 0x00B8,  # cedilla = spacing cedilla, U+00B8 ISOdia
+    "cent": 0x00A2,  # cent sign, U+00A2 ISOnum
+    "chi": 0x03C7,  # greek small letter chi, U+03C7 ISOgrk3
+    "circ": 0x02C6,  # modifier letter circumflex accent, U+02C6 ISOpub
+    "clubs": 0x2663,  # black club suit = shamrock, U+2663 ISOpub
+    "cong": 0x2245,  # approximately equal to, U+2245 ISOtech
+    "copy": 0x00A9,  # copyright sign, U+00A9 ISOnum
+    "crarr": 0x21B5,  # downwards arrow with corner leftwards = carriage return, U+21B5 NEW
+    "cup": 0x222A,  # union = cup, U+222A ISOtech
+    "curren": 0x00A4,  # currency sign, U+00A4 ISOnum
+    "dArr": 0x21D3,  # downwards double arrow, U+21D3 ISOamsa
+    "dagger": 0x2020,  # dagger, U+2020 ISOpub
+    "darr": 0x2193,  # downwards arrow, U+2193 ISOnum
+    "deg": 0x00B0,  # degree sign, U+00B0 ISOnum
+    "delta": 0x03B4,  # greek small letter delta, U+03B4 ISOgrk3
+    "diams": 0x2666,  # black diamond suit, U+2666 ISOpub
+    "divide": 0x00F7,  # division sign, U+00F7 ISOnum
+    "eacute": 0x00E9,  # latin small letter e with acute, U+00E9 ISOlat1
+    "ecirc": 0x00EA,  # latin small letter e with circumflex, U+00EA ISOlat1
+    "egrave": 0x00E8,  # latin small letter e with grave, U+00E8 ISOlat1
+    "empty": 0x2205,  # empty set = null set = diameter, U+2205 ISOamso
+    "emsp": 0x2003,  # em space, U+2003 ISOpub
+    "ensp": 0x2002,  # en space, U+2002 ISOpub
+    "epsilon": 0x03B5,  # greek small letter epsilon, U+03B5 ISOgrk3
+    "equiv": 0x2261,  # identical to, U+2261 ISOtech
+    "eta": 0x03B7,  # greek small letter eta, U+03B7 ISOgrk3
+    "eth": 0x00F0,  # latin small letter eth, U+00F0 ISOlat1
+    "euml": 0x00EB,  # latin small letter e with diaeresis, U+00EB ISOlat1
+    "euro": 0x20AC,  # euro sign, U+20AC NEW
+    "exist": 0x2203,  # there exists, U+2203 ISOtech
+    "fnof": 0x0192,  # latin small f with hook = function = florin, U+0192 ISOtech
+    "forall": 0x2200,  # for all, U+2200 ISOtech
+    "frac12": 0x00BD,  # vulgar fraction one half = fraction one half, U+00BD ISOnum
+    "frac14": 0x00BC,  # vulgar fraction one quarter = fraction one quarter, U+00BC ISOnum
+    "frac34": 0x00BE,  # vulgar fraction three quarters = fraction three quarters, U+00BE ISOnum
+    "frasl": 0x2044,  # fraction slash, U+2044 NEW
+    "gamma": 0x03B3,  # greek small letter gamma, U+03B3 ISOgrk3
+    "ge": 0x2265,  # greater-than or equal to, U+2265 ISOtech
+    "gt": 0x003E,  # greater-than sign, U+003E ISOnum
+    "hArr": 0x21D4,  # left right double arrow, U+21D4 ISOamsa
+    "harr": 0x2194,  # left right arrow, U+2194 ISOamsa
+    "hearts": 0x2665,  # black heart suit = valentine, U+2665 ISOpub
+    "hellip": 0x2026,  # horizontal ellipsis = three dot leader, U+2026 ISOpub
+    "iacute": 0x00ED,  # latin small letter i with acute, U+00ED ISOlat1
+    "icirc": 0x00EE,  # latin small letter i with circumflex, U+00EE ISOlat1
+    "iexcl": 0x00A1,  # inverted exclamation mark, U+00A1 ISOnum
+    "igrave": 0x00EC,  # latin small letter i with grave, U+00EC ISOlat1
+    "image": 0x2111,  # blackletter capital I = imaginary part, U+2111 ISOamso
+    "infin": 0x221E,  # infinity, U+221E ISOtech
+    "int": 0x222B,  # integral, U+222B ISOtech
+    "iota": 0x03B9,  # greek small letter iota, U+03B9 ISOgrk3
+    "iquest": 0x00BF,  # inverted question mark = turned question mark, U+00BF ISOnum
+    "isin": 0x2208,  # element of, U+2208 ISOtech
+    "iuml": 0x00EF,  # latin small letter i with diaeresis, U+00EF ISOlat1
+    "kappa": 0x03BA,  # greek small letter kappa, U+03BA ISOgrk3
+    "lArr": 0x21D0,  # leftwards double arrow, U+21D0 ISOtech
+    "lambda": 0x03BB,  # greek small letter lambda, U+03BB ISOgrk3
+    "lang": 0x2329,  # left-pointing angle bracket = bra, U+2329 ISOtech
+    "laquo": 0x00AB,  # left-pointing double angle quotation mark = left pointing guillemet, U+00AB ISOnum
+    "larr": 0x2190,  # leftwards arrow, U+2190 ISOnum
+    "lceil": 0x2308,  # left ceiling = apl upstile, U+2308 ISOamsc
+    "ldquo": 0x201C,  # left double quotation mark, U+201C ISOnum
+    "le": 0x2264,  # less-than or equal to, U+2264 ISOtech
+    "lfloor": 0x230A,  # left floor = apl downstile, U+230A ISOamsc
+    "lowast": 0x2217,  # asterisk operator, U+2217 ISOtech
+    "loz": 0x25CA,  # lozenge, U+25CA ISOpub
+    "lrm": 0x200E,  # left-to-right mark, U+200E NEW RFC 2070
+    "lsaquo": 0x2039,  # single left-pointing angle quotation mark, U+2039 ISO proposed
+    "lsquo": 0x2018,  # left single quotation mark, U+2018 ISOnum
+    "lt": 0x003C,  # less-than sign, U+003C ISOnum
+    "macr": 0x00AF,  # macron = spacing macron = overline = APL overbar, U+00AF ISOdia
+    "mdash": 0x2014,  # em dash, U+2014 ISOpub
+    "micro": 0x00B5,  # micro sign, U+00B5 ISOnum
+    "middot": 0x00B7,  # middle dot = Georgian comma = Greek middle dot, U+00B7 ISOnum
+    "minus": 0x2212,  # minus sign, U+2212 ISOtech
+    "mu": 0x03BC,  # greek small letter mu, U+03BC ISOgrk3
+    "nabla": 0x2207,  # nabla = backward difference, U+2207 ISOtech
+    "nbsp": 0x00A0,  # no-break space = non-breaking space, U+00A0 ISOnum
+    "ndash": 0x2013,  # en dash, U+2013 ISOpub
+    "ne": 0x2260,  # not equal to, U+2260 ISOtech
+    "ni": 0x220B,  # contains as member, U+220B ISOtech
+    "not": 0x00AC,  # not sign, U+00AC ISOnum
+    "notin": 0x2209,  # not an element of, U+2209 ISOtech
+    "nsub": 0x2284,  # not a subset of, U+2284 ISOamsn
+    "ntilde": 0x00F1,  # latin small letter n with tilde, U+00F1 ISOlat1
+    "nu": 0x03BD,  # greek small letter nu, U+03BD ISOgrk3
+    "oacute": 0x00F3,  # latin small letter o with acute, U+00F3 ISOlat1
+    "ocirc": 0x00F4,  # latin small letter o with circumflex, U+00F4 ISOlat1
+    "oelig": 0x0153,  # latin small ligature oe, U+0153 ISOlat2
+    "ograve": 0x00F2,  # latin small letter o with grave, U+00F2 ISOlat1
+    "oline": 0x203E,  # overline = spacing overscore, U+203E NEW
+    "omega": 0x03C9,  # greek small letter omega, U+03C9 ISOgrk3
+    "omicron": 0x03BF,  # greek small letter omicron, U+03BF NEW
+    "oplus": 0x2295,  # circled plus = direct sum, U+2295 ISOamsb
+    "or": 0x2228,  # logical or = vee, U+2228 ISOtech
+    "ordf": 0x00AA,  # feminine ordinal indicator, U+00AA ISOnum
+    "ordm": 0x00BA,  # masculine ordinal indicator, U+00BA ISOnum
+    "oslash": 0x00F8,  # latin small letter o with stroke, = latin small letter o slash, U+00F8 ISOlat1
+    "otilde": 0x00F5,  # latin small letter o with tilde, U+00F5 ISOlat1
+    "otimes": 0x2297,  # circled times = vector product, U+2297 ISOamsb
+    "ouml": 0x00F6,  # latin small letter o with diaeresis, U+00F6 ISOlat1
+    "para": 0x00B6,  # pilcrow sign = paragraph sign, U+00B6 ISOnum
+    "part": 0x2202,  # partial differential, U+2202 ISOtech
+    "permil": 0x2030,  # per mille sign, U+2030 ISOtech
+    "perp": 0x22A5,  # up tack = orthogonal to = perpendicular, U+22A5 ISOtech
+    "phi": 0x03C6,  # greek small letter phi, U+03C6 ISOgrk3
+    "pi": 0x03C0,  # greek small letter pi, U+03C0 ISOgrk3
+    "piv": 0x03D6,  # greek pi symbol, U+03D6 ISOgrk3
+    "plusmn": 0x00B1,  # plus-minus sign = plus-or-minus sign, U+00B1 ISOnum
+    "pound": 0x00A3,  # pound sign, U+00A3 ISOnum
+    "prime": 0x2032,  # prime = minutes = feet, U+2032 ISOtech
+    "prod": 0x220F,  # n-ary product = product sign, U+220F ISOamsb
+    "prop": 0x221D,  # proportional to, U+221D ISOtech
+    "psi": 0x03C8,  # greek small letter psi, U+03C8 ISOgrk3
+    "quot": 0x0022,  # quotation mark = APL quote, U+0022 ISOnum
+    "rArr": 0x21D2,  # rightwards double arrow, U+21D2 ISOtech
+    "radic": 0x221A,  # square root = radical sign, U+221A ISOtech
+    "rang": 0x232A,  # right-pointing angle bracket = ket, U+232A ISOtech
+    "raquo": 0x00BB,  # right-pointing double angle quotation mark = right pointing guillemet, U+00BB ISOnum
+    "rarr": 0x2192,  # rightwards arrow, U+2192 ISOnum
+    "rceil": 0x2309,  # right ceiling, U+2309 ISOamsc
+    "rdquo": 0x201D,  # right double quotation mark, U+201D ISOnum
+    "real": 0x211C,  # blackletter capital R = real part symbol, U+211C ISOamso
+    "reg": 0x00AE,  # registered sign = registered trade mark sign, U+00AE ISOnum
+    "rfloor": 0x230B,  # right floor, U+230B ISOamsc
+    "rho": 0x03C1,  # greek small letter rho, U+03C1 ISOgrk3
+    "rlm": 0x200F,  # right-to-left mark, U+200F NEW RFC 2070
+    "rsaquo": 0x203A,  # single right-pointing angle quotation mark, U+203A ISO proposed
+    "rsquo": 0x2019,  # right single quotation mark, U+2019 ISOnum
+    "sbquo": 0x201A,  # single low-9 quotation mark, U+201A NEW
+    "scaron": 0x0161,  # latin small letter s with caron, U+0161 ISOlat2
+    "sdot": 0x22C5,  # dot operator, U+22C5 ISOamsb
+    "sect": 0x00A7,  # section sign, U+00A7 ISOnum
+    "shy": 0x00AD,  # soft hyphen = discretionary hyphen, U+00AD ISOnum
+    "sigma": 0x03C3,  # greek small letter sigma, U+03C3 ISOgrk3
+    "sigmaf": 0x03C2,  # greek small letter final sigma, U+03C2 ISOgrk3
+    "sim": 0x223C,  # tilde operator = varies with = similar to, U+223C ISOtech
+    "spades": 0x2660,  # black spade suit, U+2660 ISOpub
+    "sub": 0x2282,  # subset of, U+2282 ISOtech
+    "sube": 0x2286,  # subset of or equal to, U+2286 ISOtech
+    "sum": 0x2211,  # n-ary sumation, U+2211 ISOamsb
+    "sup": 0x2283,  # superset of, U+2283 ISOtech
+    "sup1": 0x00B9,  # superscript one = superscript digit one, U+00B9 ISOnum
+    "sup2": 0x00B2,  # superscript two = superscript digit two = squared, U+00B2 ISOnum
+    "sup3": 0x00B3,  # superscript three = superscript digit three = cubed, U+00B3 ISOnum
+    "supe": 0x2287,  # superset of or equal to, U+2287 ISOtech
+    "szlig": 0x00DF,  # latin small letter sharp s = ess-zed, U+00DF ISOlat1
+    "tau": 0x03C4,  # greek small letter tau, U+03C4 ISOgrk3
+    "there4": 0x2234,  # therefore, U+2234 ISOtech
+    "theta": 0x03B8,  # greek small letter theta, U+03B8 ISOgrk3
+    "thetasym": 0x03D1,  # greek small letter theta symbol, U+03D1 NEW
+    "thinsp": 0x2009,  # thin space, U+2009 ISOpub
+    "thorn": 0x00FE,  # latin small letter thorn with, U+00FE ISOlat1
+    "tilde": 0x02DC,  # small tilde, U+02DC ISOdia
+    "times": 0x00D7,  # multiplication sign, U+00D7 ISOnum
+    "trade": 0x2122,  # trade mark sign, U+2122 ISOnum
+    "uArr": 0x21D1,  # upwards double arrow, U+21D1 ISOamsa
+    "uacute": 0x00FA,  # latin small letter u with acute, U+00FA ISOlat1
+    "uarr": 0x2191,  # upwards arrow, U+2191 ISOnum
+    "ucirc": 0x00FB,  # latin small letter u with circumflex, U+00FB ISOlat1
+    "ugrave": 0x00F9,  # latin small letter u with grave, U+00F9 ISOlat1
+    "uml": 0x00A8,  # diaeresis = spacing diaeresis, U+00A8 ISOdia
+    "upsih": 0x03D2,  # greek upsilon with hook symbol, U+03D2 NEW
+    "upsilon": 0x03C5,  # greek small letter upsilon, U+03C5 ISOgrk3
+    "uuml": 0x00FC,  # latin small letter u with diaeresis, U+00FC ISOlat1
+    "weierp": 0x2118,  # script capital P = power set = Weierstrass p, U+2118 ISOamso
+    "xi": 0x03BE,  # greek small letter xi, U+03BE ISOgrk3
+    "yacute": 0x00FD,  # latin small letter y with acute, U+00FD ISOlat1
+    "yen": 0x00A5,  # yen sign = yuan sign, U+00A5 ISOnum
+    "yuml": 0x00FF,  # latin small letter y with diaeresis, U+00FF ISOlat1
+    "zeta": 0x03B6,  # greek small letter zeta, U+03B6 ISOgrk3
+    "zwj": 0x200D,  # zero width joiner, U+200D NEW RFC 2070
+    "zwnj": 0x200C,  # zero width non-joiner, U+200C NEW RFC 2070
+}
+
+
+# maps the HTML5 named character references to the equivalent Unicode character(s)
+html5 = {
+    "Aacute": "\xc1",
+    "aacute": "\xe1",
+    "Aacute;": "\xc1",
+    "aacute;": "\xe1",
+    "Abreve;": "\u0102",
+    "abreve;": "\u0103",
+    "ac;": "\u223e",
+    "acd;": "\u223f",
+    "acE;": "\u223e\u0333",
+    "Acirc": "\xc2",
+    "acirc": "\xe2",
+    "Acirc;": "\xc2",
+    "acirc;": "\xe2",
+    "acute": "\xb4",
+    "acute;": "\xb4",
+    "Acy;": "\u0410",
+    "acy;": "\u0430",
+    "AElig": "\xc6",
+    "aelig": "\xe6",
+    "AElig;": "\xc6",
+    "aelig;": "\xe6",
+    "af;": "\u2061",
+    "Afr;": "\U0001d504",
+    "afr;": "\U0001d51e",
+    "Agrave": "\xc0",
+    "agrave": "\xe0",
+    "Agrave;": "\xc0",
+    "agrave;": "\xe0",
+    "alefsym;": "\u2135",
+    "aleph;": "\u2135",
+    "Alpha;": "\u0391",
+    "alpha;": "\u03b1",
+    "Amacr;": "\u0100",
+    "amacr;": "\u0101",
+    "amalg;": "\u2a3f",
+    "AMP": "&",
+    "amp": "&",
+    "AMP;": "&",
+    "amp;": "&",
+    "And;": "\u2a53",
+    "and;": "\u2227",
+    "andand;": "\u2a55",
+    "andd;": "\u2a5c",
+    "andslope;": "\u2a58",
+    "andv;": "\u2a5a",
+    "ang;": "\u2220",
+    "ange;": "\u29a4",
+    "angle;": "\u2220",
+    "angmsd;": "\u2221",
+    "angmsdaa;": "\u29a8",
+    "angmsdab;": "\u29a9",
+    "angmsdac;": "\u29aa",
+    "angmsdad;": "\u29ab",
+    "angmsdae;": "\u29ac",
+    "angmsdaf;": "\u29ad",
+    "angmsdag;": "\u29ae",
+    "angmsdah;": "\u29af",
+    "angrt;": "\u221f",
+    "angrtvb;": "\u22be",
+    "angrtvbd;": "\u299d",
+    "angsph;": "\u2222",
+    "angst;": "\xc5",
+    "angzarr;": "\u237c",
+    "Aogon;": "\u0104",
+    "aogon;": "\u0105",
+    "Aopf;": "\U0001d538",
+    "aopf;": "\U0001d552",
+    "ap;": "\u2248",
+    "apacir;": "\u2a6f",
+    "apE;": "\u2a70",
+    "ape;": "\u224a",
+    "apid;": "\u224b",
+    "apos;": "'",
+    "ApplyFunction;": "\u2061",
+    "approx;": "\u2248",
+    "approxeq;": "\u224a",
+    "Aring": "\xc5",
+    "aring": "\xe5",
+    "Aring;": "\xc5",
+    "aring;": "\xe5",
+    "Ascr;": "\U0001d49c",
+    "ascr;": "\U0001d4b6",
+    "Assign;": "\u2254",
+    "ast;": "*",
+    "asymp;": "\u2248",
+    "asympeq;": "\u224d",
+    "Atilde": "\xc3",
+    "atilde": "\xe3",
+    "Atilde;": "\xc3",
+    "atilde;": "\xe3",
+    "Auml": "\xc4",
+    "auml": "\xe4",
+    "Auml;": "\xc4",
+    "auml;": "\xe4",
+    "awconint;": "\u2233",
+    "awint;": "\u2a11",
+    "backcong;": "\u224c",
+    "backepsilon;": "\u03f6",
+    "backprime;": "\u2035",
+    "backsim;": "\u223d",
+    "backsimeq;": "\u22cd",
+    "Backslash;": "\u2216",
+    "Barv;": "\u2ae7",
+    "barvee;": "\u22bd",
+    "Barwed;": "\u2306",
+    "barwed;": "\u2305",
+    "barwedge;": "\u2305",
+    "bbrk;": "\u23b5",
+    "bbrktbrk;": "\u23b6",
+    "bcong;": "\u224c",
+    "Bcy;": "\u0411",
+    "bcy;": "\u0431",
+    "bdquo;": "\u201e",
+    "becaus;": "\u2235",
+    "Because;": "\u2235",
+    "because;": "\u2235",
+    "bemptyv;": "\u29b0",
+    "bepsi;": "\u03f6",
+    "bernou;": "\u212c",
+    "Bernoullis;": "\u212c",
+    "Beta;": "\u0392",
+    "beta;": "\u03b2",
+    "beth;": "\u2136",
+    "between;": "\u226c",
+    "Bfr;": "\U0001d505",
+    "bfr;": "\U0001d51f",
+    "bigcap;": "\u22c2",
+    "bigcirc;": "\u25ef",
+    "bigcup;": "\u22c3",
+    "bigodot;": "\u2a00",
+    "bigoplus;": "\u2a01",
+    "bigotimes;": "\u2a02",
+    "bigsqcup;": "\u2a06",
+    "bigstar;": "\u2605",
+    "bigtriangledown;": "\u25bd",
+    "bigtriangleup;": "\u25b3",
+    "biguplus;": "\u2a04",
+    "bigvee;": "\u22c1",
+    "bigwedge;": "\u22c0",
+    "bkarow;": "\u290d",
+    "blacklozenge;": "\u29eb",
+    "blacksquare;": "\u25aa",
+    "blacktriangle;": "\u25b4",
+    "blacktriangledown;": "\u25be",
+    "blacktriangleleft;": "\u25c2",
+    "blacktriangleright;": "\u25b8",
+    "blank;": "\u2423",
+    "blk12;": "\u2592",
+    "blk14;": "\u2591",
+    "blk34;": "\u2593",
+    "block;": "\u2588",
+    "bne;": "=\u20e5",
+    "bnequiv;": "\u2261\u20e5",
+    "bNot;": "\u2aed",
+    "bnot;": "\u2310",
+    "Bopf;": "\U0001d539",
+    "bopf;": "\U0001d553",
+    "bot;": "\u22a5",
+    "bottom;": "\u22a5",
+    "bowtie;": "\u22c8",
+    "boxbox;": "\u29c9",
+    "boxDL;": "\u2557",
+    "boxDl;": "\u2556",
+    "boxdL;": "\u2555",
+    "boxdl;": "\u2510",
+    "boxDR;": "\u2554",
+    "boxDr;": "\u2553",
+    "boxdR;": "\u2552",
+    "boxdr;": "\u250c",
+    "boxH;": "\u2550",
+    "boxh;": "\u2500",
+    "boxHD;": "\u2566",
+    "boxHd;": "\u2564",
+    "boxhD;": "\u2565",
+    "boxhd;": "\u252c",
+    "boxHU;": "\u2569",
+    "boxHu;": "\u2567",
+    "boxhU;": "\u2568",
+    "boxhu;": "\u2534",
+    "boxminus;": "\u229f",
+    "boxplus;": "\u229e",
+    "boxtimes;": "\u22a0",
+    "boxUL;": "\u255d",
+    "boxUl;": "\u255c",
+    "boxuL;": "\u255b",
+    "boxul;": "\u2518",
+    "boxUR;": "\u255a",
+    "boxUr;": "\u2559",
+    "boxuR;": "\u2558",
+    "boxur;": "\u2514",
+    "boxV;": "\u2551",
+    "boxv;": "\u2502",
+    "boxVH;": "\u256c",
+    "boxVh;": "\u256b",
+    "boxvH;": "\u256a",
+    "boxvh;": "\u253c",
+    "boxVL;": "\u2563",
+    "boxVl;": "\u2562",
+    "boxvL;": "\u2561",
+    "boxvl;": "\u2524",
+    "boxVR;": "\u2560",
+    "boxVr;": "\u255f",
+    "boxvR;": "\u255e",
+    "boxvr;": "\u251c",
+    "bprime;": "\u2035",
+    "Breve;": "\u02d8",
+    "breve;": "\u02d8",
+    "brvbar": "\xa6",
+    "brvbar;": "\xa6",
+    "Bscr;": "\u212c",
+    "bscr;": "\U0001d4b7",
+    "bsemi;": "\u204f",
+    "bsim;": "\u223d",
+    "bsime;": "\u22cd",
+    "bsol;": "\\",
+    "bsolb;": "\u29c5",
+    "bsolhsub;": "\u27c8",
+    "bull;": "\u2022",
+    "bullet;": "\u2022",
+    "bump;": "\u224e",
+    "bumpE;": "\u2aae",
+    "bumpe;": "\u224f",
+    "Bumpeq;": "\u224e",
+    "bumpeq;": "\u224f",
+    "Cacute;": "\u0106",
+    "cacute;": "\u0107",
+    "Cap;": "\u22d2",
+    "cap;": "\u2229",
+    "capand;": "\u2a44",
+    "capbrcup;": "\u2a49",
+    "capcap;": "\u2a4b",
+    "capcup;": "\u2a47",
+    "capdot;": "\u2a40",
+    "CapitalDifferentialD;": "\u2145",
+    "caps;": "\u2229\ufe00",
+    "caret;": "\u2041",
+    "caron;": "\u02c7",
+    "Cayleys;": "\u212d",
+    "ccaps;": "\u2a4d",
+    "Ccaron;": "\u010c",
+    "ccaron;": "\u010d",
+    "Ccedil": "\xc7",
+    "ccedil": "\xe7",
+    "Ccedil;": "\xc7",
+    "ccedil;": "\xe7",
+    "Ccirc;": "\u0108",
+    "ccirc;": "\u0109",
+    "Cconint;": "\u2230",
+    "ccups;": "\u2a4c",
+    "ccupssm;": "\u2a50",
+    "Cdot;": "\u010a",
+    "cdot;": "\u010b",
+    "cedil": "\xb8",
+    "cedil;": "\xb8",
+    "Cedilla;": "\xb8",
+    "cemptyv;": "\u29b2",
+    "cent": "\xa2",
+    "cent;": "\xa2",
+    "CenterDot;": "\xb7",
+    "centerdot;": "\xb7",
+    "Cfr;": "\u212d",
+    "cfr;": "\U0001d520",
+    "CHcy;": "\u0427",
+    "chcy;": "\u0447",
+    "check;": "\u2713",
+    "checkmark;": "\u2713",
+    "Chi;": "\u03a7",
+    "chi;": "\u03c7",
+    "cir;": "\u25cb",
+    "circ;": "\u02c6",
+    "circeq;": "\u2257",
+    "circlearrowleft;": "\u21ba",
+    "circlearrowright;": "\u21bb",
+    "circledast;": "\u229b",
+    "circledcirc;": "\u229a",
+    "circleddash;": "\u229d",
+    "CircleDot;": "\u2299",
+    "circledR;": "\xae",
+    "circledS;": "\u24c8",
+    "CircleMinus;": "\u2296",
+    "CirclePlus;": "\u2295",
+    "CircleTimes;": "\u2297",
+    "cirE;": "\u29c3",
+    "cire;": "\u2257",
+    "cirfnint;": "\u2a10",
+    "cirmid;": "\u2aef",
+    "cirscir;": "\u29c2",
+    "ClockwiseContourIntegral;": "\u2232",
+    "CloseCurlyDoubleQuote;": "\u201d",
+    "CloseCurlyQuote;": "\u2019",
+    "clubs;": "\u2663",
+    "clubsuit;": "\u2663",
+    "Colon;": "\u2237",
+    "colon;": ":",
+    "Colone;": "\u2a74",
+    "colone;": "\u2254",
+    "coloneq;": "\u2254",
+    "comma;": ",",
+    "commat;": "@",
+    "comp;": "\u2201",
+    "compfn;": "\u2218",
+    "complement;": "\u2201",
+    "complexes;": "\u2102",
+    "cong;": "\u2245",
+    "congdot;": "\u2a6d",
+    "Congruent;": "\u2261",
+    "Conint;": "\u222f",
+    "conint;": "\u222e",
+    "ContourIntegral;": "\u222e",
+    "Copf;": "\u2102",
+    "copf;": "\U0001d554",
+    "coprod;": "\u2210",
+    "Coproduct;": "\u2210",
+    "COPY": "\xa9",
+    "copy": "\xa9",
+    "COPY;": "\xa9",
+    "copy;": "\xa9",
+    "copysr;": "\u2117",
+    "CounterClockwiseContourIntegral;": "\u2233",
+    "crarr;": "\u21b5",
+    "Cross;": "\u2a2f",
+    "cross;": "\u2717",
+    "Cscr;": "\U0001d49e",
+    "cscr;": "\U0001d4b8",
+    "csub;": "\u2acf",
+    "csube;": "\u2ad1",
+    "csup;": "\u2ad0",
+    "csupe;": "\u2ad2",
+    "ctdot;": "\u22ef",
+    "cudarrl;": "\u2938",
+    "cudarrr;": "\u2935",
+    "cuepr;": "\u22de",
+    "cuesc;": "\u22df",
+    "cularr;": "\u21b6",
+    "cularrp;": "\u293d",
+    "Cup;": "\u22d3",
+    "cup;": "\u222a",
+    "cupbrcap;": "\u2a48",
+    "CupCap;": "\u224d",
+    "cupcap;": "\u2a46",
+    "cupcup;": "\u2a4a",
+    "cupdot;": "\u228d",
+    "cupor;": "\u2a45",
+    "cups;": "\u222a\ufe00",
+    "curarr;": "\u21b7",
+    "curarrm;": "\u293c",
+    "curlyeqprec;": "\u22de",
+    "curlyeqsucc;": "\u22df",
+    "curlyvee;": "\u22ce",
+    "curlywedge;": "\u22cf",
+    "curren": "\xa4",
+    "curren;": "\xa4",
+    "curvearrowleft;": "\u21b6",
+    "curvearrowright;": "\u21b7",
+    "cuvee;": "\u22ce",
+    "cuwed;": "\u22cf",
+    "cwconint;": "\u2232",
+    "cwint;": "\u2231",
+    "cylcty;": "\u232d",
+    "Dagger;": "\u2021",
+    "dagger;": "\u2020",
+    "daleth;": "\u2138",
+    "Darr;": "\u21a1",
+    "dArr;": "\u21d3",
+    "darr;": "\u2193",
+    "dash;": "\u2010",
+    "Dashv;": "\u2ae4",
+    "dashv;": "\u22a3",
+    "dbkarow;": "\u290f",
+    "dblac;": "\u02dd",
+    "Dcaron;": "\u010e",
+    "dcaron;": "\u010f",
+    "Dcy;": "\u0414",
+    "dcy;": "\u0434",
+    "DD;": "\u2145",
+    "dd;": "\u2146",
+    "ddagger;": "\u2021",
+    "ddarr;": "\u21ca",
+    "DDotrahd;": "\u2911",
+    "ddotseq;": "\u2a77",
+    "deg": "\xb0",
+    "deg;": "\xb0",
+    "Del;": "\u2207",
+    "Delta;": "\u0394",
+    "delta;": "\u03b4",
+    "demptyv;": "\u29b1",
+    "dfisht;": "\u297f",
+    "Dfr;": "\U0001d507",
+    "dfr;": "\U0001d521",
+    "dHar;": "\u2965",
+    "dharl;": "\u21c3",
+    "dharr;": "\u21c2",
+    "DiacriticalAcute;": "\xb4",
+    "DiacriticalDot;": "\u02d9",
+    "DiacriticalDoubleAcute;": "\u02dd",
+    "DiacriticalGrave;": "`",
+    "DiacriticalTilde;": "\u02dc",
+    "diam;": "\u22c4",
+    "Diamond;": "\u22c4",
+    "diamond;": "\u22c4",
+    "diamondsuit;": "\u2666",
+    "diams;": "\u2666",
+    "die;": "\xa8",
+    "DifferentialD;": "\u2146",
+    "digamma;": "\u03dd",
+    "disin;": "\u22f2",
+    "div;": "\xf7",
+    "divide": "\xf7",
+    "divide;": "\xf7",
+    "divideontimes;": "\u22c7",
+    "divonx;": "\u22c7",
+    "DJcy;": "\u0402",
+    "djcy;": "\u0452",
+    "dlcorn;": "\u231e",
+    "dlcrop;": "\u230d",
+    "dollar;": "$",
+    "Dopf;": "\U0001d53b",
+    "dopf;": "\U0001d555",
+    "Dot;": "\xa8",
+    "dot;": "\u02d9",
+    "DotDot;": "\u20dc",
+    "doteq;": "\u2250",
+    "doteqdot;": "\u2251",
+    "DotEqual;": "\u2250",
+    "dotminus;": "\u2238",
+    "dotplus;": "\u2214",
+    "dotsquare;": "\u22a1",
+    "doublebarwedge;": "\u2306",
+    "DoubleContourIntegral;": "\u222f",
+    "DoubleDot;": "\xa8",
+    "DoubleDownArrow;": "\u21d3",
+    "DoubleLeftArrow;": "\u21d0",
+    "DoubleLeftRightArrow;": "\u21d4",
+    "DoubleLeftTee;": "\u2ae4",
+    "DoubleLongLeftArrow;": "\u27f8",
+    "DoubleLongLeftRightArrow;": "\u27fa",
+    "DoubleLongRightArrow;": "\u27f9",
+    "DoubleRightArrow;": "\u21d2",
+    "DoubleRightTee;": "\u22a8",
+    "DoubleUpArrow;": "\u21d1",
+    "DoubleUpDownArrow;": "\u21d5",
+    "DoubleVerticalBar;": "\u2225",
+    "DownArrow;": "\u2193",
+    "Downarrow;": "\u21d3",
+    "downarrow;": "\u2193",
+    "DownArrowBar;": "\u2913",
+    "DownArrowUpArrow;": "\u21f5",
+    "DownBreve;": "\u0311",
+    "downdownarrows;": "\u21ca",
+    "downharpoonleft;": "\u21c3",
+    "downharpoonright;": "\u21c2",
+    "DownLeftRightVector;": "\u2950",
+    "DownLeftTeeVector;": "\u295e",
+    "DownLeftVector;": "\u21bd",
+    "DownLeftVectorBar;": "\u2956",
+    "DownRightTeeVector;": "\u295f",
+    "DownRightVector;": "\u21c1",
+    "DownRightVectorBar;": "\u2957",
+    "DownTee;": "\u22a4",
+    "DownTeeArrow;": "\u21a7",
+    "drbkarow;": "\u2910",
+    "drcorn;": "\u231f",
+    "drcrop;": "\u230c",
+    "Dscr;": "\U0001d49f",
+    "dscr;": "\U0001d4b9",
+    "DScy;": "\u0405",
+    "dscy;": "\u0455",
+    "dsol;": "\u29f6",
+    "Dstrok;": "\u0110",
+    "dstrok;": "\u0111",
+    "dtdot;": "\u22f1",
+    "dtri;": "\u25bf",
+    "dtrif;": "\u25be",
+    "duarr;": "\u21f5",
+    "duhar;": "\u296f",
+    "dwangle;": "\u29a6",
+    "DZcy;": "\u040f",
+    "dzcy;": "\u045f",
+    "dzigrarr;": "\u27ff",
+    "Eacute": "\xc9",
+    "eacute": "\xe9",
+    "Eacute;": "\xc9",
+    "eacute;": "\xe9",
+    "easter;": "\u2a6e",
+    "Ecaron;": "\u011a",
+    "ecaron;": "\u011b",
+    "ecir;": "\u2256",
+    "Ecirc": "\xca",
+    "ecirc": "\xea",
+    "Ecirc;": "\xca",
+    "ecirc;": "\xea",
+    "ecolon;": "\u2255",
+    "Ecy;": "\u042d",
+    "ecy;": "\u044d",
+    "eDDot;": "\u2a77",
+    "Edot;": "\u0116",
+    "eDot;": "\u2251",
+    "edot;": "\u0117",
+    "ee;": "\u2147",
+    "efDot;": "\u2252",
+    "Efr;": "\U0001d508",
+    "efr;": "\U0001d522",
+    "eg;": "\u2a9a",
+    "Egrave": "\xc8",
+    "egrave": "\xe8",
+    "Egrave;": "\xc8",
+    "egrave;": "\xe8",
+    "egs;": "\u2a96",
+    "egsdot;": "\u2a98",
+    "el;": "\u2a99",
+    "Element;": "\u2208",
+    "elinters;": "\u23e7",
+    "ell;": "\u2113",
+    "els;": "\u2a95",
+    "elsdot;": "\u2a97",
+    "Emacr;": "\u0112",
+    "emacr;": "\u0113",
+    "empty;": "\u2205",
+    "emptyset;": "\u2205",
+    "EmptySmallSquare;": "\u25fb",
+    "emptyv;": "\u2205",
+    "EmptyVerySmallSquare;": "\u25ab",
+    "emsp13;": "\u2004",
+    "emsp14;": "\u2005",
+    "emsp;": "\u2003",
+    "ENG;": "\u014a",
+    "eng;": "\u014b",
+    "ensp;": "\u2002",
+    "Eogon;": "\u0118",
+    "eogon;": "\u0119",
+    "Eopf;": "\U0001d53c",
+    "eopf;": "\U0001d556",
+    "epar;": "\u22d5",
+    "eparsl;": "\u29e3",
+    "eplus;": "\u2a71",
+    "epsi;": "\u03b5",
+    "Epsilon;": "\u0395",
+    "epsilon;": "\u03b5",
+    "epsiv;": "\u03f5",
+    "eqcirc;": "\u2256",
+    "eqcolon;": "\u2255",
+    "eqsim;": "\u2242",
+    "eqslantgtr;": "\u2a96",
+    "eqslantless;": "\u2a95",
+    "Equal;": "\u2a75",
+    "equals;": "=",
+    "EqualTilde;": "\u2242",
+    "equest;": "\u225f",
+    "Equilibrium;": "\u21cc",
+    "equiv;": "\u2261",
+    "equivDD;": "\u2a78",
+    "eqvparsl;": "\u29e5",
+    "erarr;": "\u2971",
+    "erDot;": "\u2253",
+    "Escr;": "\u2130",
+    "escr;": "\u212f",
+    "esdot;": "\u2250",
+    "Esim;": "\u2a73",
+    "esim;": "\u2242",
+    "Eta;": "\u0397",
+    "eta;": "\u03b7",
+    "ETH": "\xd0",
+    "eth": "\xf0",
+    "ETH;": "\xd0",
+    "eth;": "\xf0",
+    "Euml": "\xcb",
+    "euml": "\xeb",
+    "Euml;": "\xcb",
+    "euml;": "\xeb",
+    "euro;": "\u20ac",
+    "excl;": "!",
+    "exist;": "\u2203",
+    "Exists;": "\u2203",
+    "expectation;": "\u2130",
+    "ExponentialE;": "\u2147",
+    "exponentiale;": "\u2147",
+    "fallingdotseq;": "\u2252",
+    "Fcy;": "\u0424",
+    "fcy;": "\u0444",
+    "female;": "\u2640",
+    "ffilig;": "\ufb03",
+    "fflig;": "\ufb00",
+    "ffllig;": "\ufb04",
+    "Ffr;": "\U0001d509",
+    "ffr;": "\U0001d523",
+    "filig;": "\ufb01",
+    "FilledSmallSquare;": "\u25fc",
+    "FilledVerySmallSquare;": "\u25aa",
+    "fjlig;": "fj",
+    "flat;": "\u266d",
+    "fllig;": "\ufb02",
+    "fltns;": "\u25b1",
+    "fnof;": "\u0192",
+    "Fopf;": "\U0001d53d",
+    "fopf;": "\U0001d557",
+    "ForAll;": "\u2200",
+    "forall;": "\u2200",
+    "fork;": "\u22d4",
+    "forkv;": "\u2ad9",
+    "Fouriertrf;": "\u2131",
+    "fpartint;": "\u2a0d",
+    "frac12": "\xbd",
+    "frac12;": "\xbd",
+    "frac13;": "\u2153",
+    "frac14": "\xbc",
+    "frac14;": "\xbc",
+    "frac15;": "\u2155",
+    "frac16;": "\u2159",
+    "frac18;": "\u215b",
+    "frac23;": "\u2154",
+    "frac25;": "\u2156",
+    "frac34": "\xbe",
+    "frac34;": "\xbe",
+    "frac35;": "\u2157",
+    "frac38;": "\u215c",
+    "frac45;": "\u2158",
+    "frac56;": "\u215a",
+    "frac58;": "\u215d",
+    "frac78;": "\u215e",
+    "frasl;": "\u2044",
+    "frown;": "\u2322",
+    "Fscr;": "\u2131",
+    "fscr;": "\U0001d4bb",
+    "gacute;": "\u01f5",
+    "Gamma;": "\u0393",
+    "gamma;": "\u03b3",
+    "Gammad;": "\u03dc",
+    "gammad;": "\u03dd",
+    "gap;": "\u2a86",
+    "Gbreve;": "\u011e",
+    "gbreve;": "\u011f",
+    "Gcedil;": "\u0122",
+    "Gcirc;": "\u011c",
+    "gcirc;": "\u011d",
+    "Gcy;": "\u0413",
+    "gcy;": "\u0433",
+    "Gdot;": "\u0120",
+    "gdot;": "\u0121",
+    "gE;": "\u2267",
+    "ge;": "\u2265",
+    "gEl;": "\u2a8c",
+    "gel;": "\u22db",
+    "geq;": "\u2265",
+    "geqq;": "\u2267",
+    "geqslant;": "\u2a7e",
+    "ges;": "\u2a7e",
+    "gescc;": "\u2aa9",
+    "gesdot;": "\u2a80",
+    "gesdoto;": "\u2a82",
+    "gesdotol;": "\u2a84",
+    "gesl;": "\u22db\ufe00",
+    "gesles;": "\u2a94",
+    "Gfr;": "\U0001d50a",
+    "gfr;": "\U0001d524",
+    "Gg;": "\u22d9",
+    "gg;": "\u226b",
+    "ggg;": "\u22d9",
+    "gimel;": "\u2137",
+    "GJcy;": "\u0403",
+    "gjcy;": "\u0453",
+    "gl;": "\u2277",
+    "gla;": "\u2aa5",
+    "glE;": "\u2a92",
+    "glj;": "\u2aa4",
+    "gnap;": "\u2a8a",
+    "gnapprox;": "\u2a8a",
+    "gnE;": "\u2269",
+    "gne;": "\u2a88",
+    "gneq;": "\u2a88",
+    "gneqq;": "\u2269",
+    "gnsim;": "\u22e7",
+    "Gopf;": "\U0001d53e",
+    "gopf;": "\U0001d558",
+    "grave;": "`",
+    "GreaterEqual;": "\u2265",
+    "GreaterEqualLess;": "\u22db",
+    "GreaterFullEqual;": "\u2267",
+    "GreaterGreater;": "\u2aa2",
+    "GreaterLess;": "\u2277",
+    "GreaterSlantEqual;": "\u2a7e",
+    "GreaterTilde;": "\u2273",
+    "Gscr;": "\U0001d4a2",
+    "gscr;": "\u210a",
+    "gsim;": "\u2273",
+    "gsime;": "\u2a8e",
+    "gsiml;": "\u2a90",
+    "GT": ">",
+    "gt": ">",
+    "GT;": ">",
+    "Gt;": "\u226b",
+    "gt;": ">",
+    "gtcc;": "\u2aa7",
+    "gtcir;": "\u2a7a",
+    "gtdot;": "\u22d7",
+    "gtlPar;": "\u2995",
+    "gtquest;": "\u2a7c",
+    "gtrapprox;": "\u2a86",
+    "gtrarr;": "\u2978",
+    "gtrdot;": "\u22d7",
+    "gtreqless;": "\u22db",
+    "gtreqqless;": "\u2a8c",
+    "gtrless;": "\u2277",
+    "gtrsim;": "\u2273",
+    "gvertneqq;": "\u2269\ufe00",
+    "gvnE;": "\u2269\ufe00",
+    "Hacek;": "\u02c7",
+    "hairsp;": "\u200a",
+    "half;": "\xbd",
+    "hamilt;": "\u210b",
+    "HARDcy;": "\u042a",
+    "hardcy;": "\u044a",
+    "hArr;": "\u21d4",
+    "harr;": "\u2194",
+    "harrcir;": "\u2948",
+    "harrw;": "\u21ad",
+    "Hat;": "^",
+    "hbar;": "\u210f",
+    "Hcirc;": "\u0124",
+    "hcirc;": "\u0125",
+    "hearts;": "\u2665",
+    "heartsuit;": "\u2665",
+    "hellip;": "\u2026",
+    "hercon;": "\u22b9",
+    "Hfr;": "\u210c",
+    "hfr;": "\U0001d525",
+    "HilbertSpace;": "\u210b",
+    "hksearow;": "\u2925",
+    "hkswarow;": "\u2926",
+    "hoarr;": "\u21ff",
+    "homtht;": "\u223b",
+    "hookleftarrow;": "\u21a9",
+    "hookrightarrow;": "\u21aa",
+    "Hopf;": "\u210d",
+    "hopf;": "\U0001d559",
+    "horbar;": "\u2015",
+    "HorizontalLine;": "\u2500",
+    "Hscr;": "\u210b",
+    "hscr;": "\U0001d4bd",
+    "hslash;": "\u210f",
+    "Hstrok;": "\u0126",
+    "hstrok;": "\u0127",
+    "HumpDownHump;": "\u224e",
+    "HumpEqual;": "\u224f",
+    "hybull;": "\u2043",
+    "hyphen;": "\u2010",
+    "Iacute": "\xcd",
+    "iacute": "\xed",
+    "Iacute;": "\xcd",
+    "iacute;": "\xed",
+    "ic;": "\u2063",
+    "Icirc": "\xce",
+    "icirc": "\xee",
+    "Icirc;": "\xce",
+    "icirc;": "\xee",
+    "Icy;": "\u0418",
+    "icy;": "\u0438",
+    "Idot;": "\u0130",
+    "IEcy;": "\u0415",
+    "iecy;": "\u0435",
+    "iexcl": "\xa1",
+    "iexcl;": "\xa1",
+    "iff;": "\u21d4",
+    "Ifr;": "\u2111",
+    "ifr;": "\U0001d526",
+    "Igrave": "\xcc",
+    "igrave": "\xec",
+    "Igrave;": "\xcc",
+    "igrave;": "\xec",
+    "ii;": "\u2148",
+    "iiiint;": "\u2a0c",
+    "iiint;": "\u222d",
+    "iinfin;": "\u29dc",
+    "iiota;": "\u2129",
+    "IJlig;": "\u0132",
+    "ijlig;": "\u0133",
+    "Im;": "\u2111",
+    "Imacr;": "\u012a",
+    "imacr;": "\u012b",
+    "image;": "\u2111",
+    "ImaginaryI;": "\u2148",
+    "imagline;": "\u2110",
+    "imagpart;": "\u2111",
+    "imath;": "\u0131",
+    "imof;": "\u22b7",
+    "imped;": "\u01b5",
+    "Implies;": "\u21d2",
+    "in;": "\u2208",
+    "incare;": "\u2105",
+    "infin;": "\u221e",
+    "infintie;": "\u29dd",
+    "inodot;": "\u0131",
+    "Int;": "\u222c",
+    "int;": "\u222b",
+    "intcal;": "\u22ba",
+    "integers;": "\u2124",
+    "Integral;": "\u222b",
+    "intercal;": "\u22ba",
+    "Intersection;": "\u22c2",
+    "intlarhk;": "\u2a17",
+    "intprod;": "\u2a3c",
+    "InvisibleComma;": "\u2063",
+    "InvisibleTimes;": "\u2062",
+    "IOcy;": "\u0401",
+    "iocy;": "\u0451",
+    "Iogon;": "\u012e",
+    "iogon;": "\u012f",
+    "Iopf;": "\U0001d540",
+    "iopf;": "\U0001d55a",
+    "Iota;": "\u0399",
+    "iota;": "\u03b9",
+    "iprod;": "\u2a3c",
+    "iquest": "\xbf",
+    "iquest;": "\xbf",
+    "Iscr;": "\u2110",
+    "iscr;": "\U0001d4be",
+    "isin;": "\u2208",
+    "isindot;": "\u22f5",
+    "isinE;": "\u22f9",
+    "isins;": "\u22f4",
+    "isinsv;": "\u22f3",
+    "isinv;": "\u2208",
+    "it;": "\u2062",
+    "Itilde;": "\u0128",
+    "itilde;": "\u0129",
+    "Iukcy;": "\u0406",
+    "iukcy;": "\u0456",
+    "Iuml": "\xcf",
+    "iuml": "\xef",
+    "Iuml;": "\xcf",
+    "iuml;": "\xef",
+    "Jcirc;": "\u0134",
+    "jcirc;": "\u0135",
+    "Jcy;": "\u0419",
+    "jcy;": "\u0439",
+    "Jfr;": "\U0001d50d",
+    "jfr;": "\U0001d527",
+    "jmath;": "\u0237",
+    "Jopf;": "\U0001d541",
+    "jopf;": "\U0001d55b",
+    "Jscr;": "\U0001d4a5",
+    "jscr;": "\U0001d4bf",
+    "Jsercy;": "\u0408",
+    "jsercy;": "\u0458",
+    "Jukcy;": "\u0404",
+    "jukcy;": "\u0454",
+    "Kappa;": "\u039a",
+    "kappa;": "\u03ba",
+    "kappav;": "\u03f0",
+    "Kcedil;": "\u0136",
+    "kcedil;": "\u0137",
+    "Kcy;": "\u041a",
+    "kcy;": "\u043a",
+    "Kfr;": "\U0001d50e",
+    "kfr;": "\U0001d528",
+    "kgreen;": "\u0138",
+    "KHcy;": "\u0425",
+    "khcy;": "\u0445",
+    "KJcy;": "\u040c",
+    "kjcy;": "\u045c",
+    "Kopf;": "\U0001d542",
+    "kopf;": "\U0001d55c",
+    "Kscr;": "\U0001d4a6",
+    "kscr;": "\U0001d4c0",
+    "lAarr;": "\u21da",
+    "Lacute;": "\u0139",
+    "lacute;": "\u013a",
+    "laemptyv;": "\u29b4",
+    "lagran;": "\u2112",
+    "Lambda;": "\u039b",
+    "lambda;": "\u03bb",
+    "Lang;": "\u27ea",
+    "lang;": "\u27e8",
+    "langd;": "\u2991",
+    "langle;": "\u27e8",
+    "lap;": "\u2a85",
+    "Laplacetrf;": "\u2112",
+    "laquo": "\xab",
+    "laquo;": "\xab",
+    "Larr;": "\u219e",
+    "lArr;": "\u21d0",
+    "larr;": "\u2190",
+    "larrb;": "\u21e4",
+    "larrbfs;": "\u291f",
+    "larrfs;": "\u291d",
+    "larrhk;": "\u21a9",
+    "larrlp;": "\u21ab",
+    "larrpl;": "\u2939",
+    "larrsim;": "\u2973",
+    "larrtl;": "\u21a2",
+    "lat;": "\u2aab",
+    "lAtail;": "\u291b",
+    "latail;": "\u2919",
+    "late;": "\u2aad",
+    "lates;": "\u2aad\ufe00",
+    "lBarr;": "\u290e",
+    "lbarr;": "\u290c",
+    "lbbrk;": "\u2772",
+    "lbrace;": "{",
+    "lbrack;": "[",
+    "lbrke;": "\u298b",
+    "lbrksld;": "\u298f",
+    "lbrkslu;": "\u298d",
+    "Lcaron;": "\u013d",
+    "lcaron;": "\u013e",
+    "Lcedil;": "\u013b",
+    "lcedil;": "\u013c",
+    "lceil;": "\u2308",
+    "lcub;": "{",
+    "Lcy;": "\u041b",
+    "lcy;": "\u043b",
+    "ldca;": "\u2936",
+    "ldquo;": "\u201c",
+    "ldquor;": "\u201e",
+    "ldrdhar;": "\u2967",
+    "ldrushar;": "\u294b",
+    "ldsh;": "\u21b2",
+    "lE;": "\u2266",
+    "le;": "\u2264",
+    "LeftAngleBracket;": "\u27e8",
+    "LeftArrow;": "\u2190",
+    "Leftarrow;": "\u21d0",
+    "leftarrow;": "\u2190",
+    "LeftArrowBar;": "\u21e4",
+    "LeftArrowRightArrow;": "\u21c6",
+    "leftarrowtail;": "\u21a2",
+    "LeftCeiling;": "\u2308",
+    "LeftDoubleBracket;": "\u27e6",
+    "LeftDownTeeVector;": "\u2961",
+    "LeftDownVector;": "\u21c3",
+    "LeftDownVectorBar;": "\u2959",
+    "LeftFloor;": "\u230a",
+    "leftharpoondown;": "\u21bd",
+    "leftharpoonup;": "\u21bc",
+    "leftleftarrows;": "\u21c7",
+    "LeftRightArrow;": "\u2194",
+    "Leftrightarrow;": "\u21d4",
+    "leftrightarrow;": "\u2194",
+    "leftrightarrows;": "\u21c6",
+    "leftrightharpoons;": "\u21cb",
+    "leftrightsquigarrow;": "\u21ad",
+    "LeftRightVector;": "\u294e",
+    "LeftTee;": "\u22a3",
+    "LeftTeeArrow;": "\u21a4",
+    "LeftTeeVector;": "\u295a",
+    "leftthreetimes;": "\u22cb",
+    "LeftTriangle;": "\u22b2",
+    "LeftTriangleBar;": "\u29cf",
+    "LeftTriangleEqual;": "\u22b4",
+    "LeftUpDownVector;": "\u2951",
+    "LeftUpTeeVector;": "\u2960",
+    "LeftUpVector;": "\u21bf",
+    "LeftUpVectorBar;": "\u2958",
+    "LeftVector;": "\u21bc",
+    "LeftVectorBar;": "\u2952",
+    "lEg;": "\u2a8b",
+    "leg;": "\u22da",
+    "leq;": "\u2264",
+    "leqq;": "\u2266",
+    "leqslant;": "\u2a7d",
+    "les;": "\u2a7d",
+    "lescc;": "\u2aa8",
+    "lesdot;": "\u2a7f",
+    "lesdoto;": "\u2a81",
+    "lesdotor;": "\u2a83",
+    "lesg;": "\u22da\ufe00",
+    "lesges;": "\u2a93",
+    "lessapprox;": "\u2a85",
+    "lessdot;": "\u22d6",
+    "lesseqgtr;": "\u22da",
+    "lesseqqgtr;": "\u2a8b",
+    "LessEqualGreater;": "\u22da",
+    "LessFullEqual;": "\u2266",
+    "LessGreater;": "\u2276",
+    "lessgtr;": "\u2276",
+    "LessLess;": "\u2aa1",
+    "lesssim;": "\u2272",
+    "LessSlantEqual;": "\u2a7d",
+    "LessTilde;": "\u2272",
+    "lfisht;": "\u297c",
+    "lfloor;": "\u230a",
+    "Lfr;": "\U0001d50f",
+    "lfr;": "\U0001d529",
+    "lg;": "\u2276",
+    "lgE;": "\u2a91",
+    "lHar;": "\u2962",
+    "lhard;": "\u21bd",
+    "lharu;": "\u21bc",
+    "lharul;": "\u296a",
+    "lhblk;": "\u2584",
+    "LJcy;": "\u0409",
+    "ljcy;": "\u0459",
+    "Ll;": "\u22d8",
+    "ll;": "\u226a",
+    "llarr;": "\u21c7",
+    "llcorner;": "\u231e",
+    "Lleftarrow;": "\u21da",
+    "llhard;": "\u296b",
+    "lltri;": "\u25fa",
+    "Lmidot;": "\u013f",
+    "lmidot;": "\u0140",
+    "lmoust;": "\u23b0",
+    "lmoustache;": "\u23b0",
+    "lnap;": "\u2a89",
+    "lnapprox;": "\u2a89",
+    "lnE;": "\u2268",
+    "lne;": "\u2a87",
+    "lneq;": "\u2a87",
+    "lneqq;": "\u2268",
+    "lnsim;": "\u22e6",
+    "loang;": "\u27ec",
+    "loarr;": "\u21fd",
+    "lobrk;": "\u27e6",
+    "LongLeftArrow;": "\u27f5",
+    "Longleftarrow;": "\u27f8",
+    "longleftarrow;": "\u27f5",
+    "LongLeftRightArrow;": "\u27f7",
+    "Longleftrightarrow;": "\u27fa",
+    "longleftrightarrow;": "\u27f7",
+    "longmapsto;": "\u27fc",
+    "LongRightArrow;": "\u27f6",
+    "Longrightarrow;": "\u27f9",
+    "longrightarrow;": "\u27f6",
+    "looparrowleft;": "\u21ab",
+    "looparrowright;": "\u21ac",
+    "lopar;": "\u2985",
+    "Lopf;": "\U0001d543",
+    "lopf;": "\U0001d55d",
+    "loplus;": "\u2a2d",
+    "lotimes;": "\u2a34",
+    "lowast;": "\u2217",
+    "lowbar;": "_",
+    "LowerLeftArrow;": "\u2199",
+    "LowerRightArrow;": "\u2198",
+    "loz;": "\u25ca",
+    "lozenge;": "\u25ca",
+    "lozf;": "\u29eb",
+    "lpar;": "(",
+    "lparlt;": "\u2993",
+    "lrarr;": "\u21c6",
+    "lrcorner;": "\u231f",
+    "lrhar;": "\u21cb",
+    "lrhard;": "\u296d",
+    "lrm;": "\u200e",
+    "lrtri;": "\u22bf",
+    "lsaquo;": "\u2039",
+    "Lscr;": "\u2112",
+    "lscr;": "\U0001d4c1",
+    "Lsh;": "\u21b0",
+    "lsh;": "\u21b0",
+    "lsim;": "\u2272",
+    "lsime;": "\u2a8d",
+    "lsimg;": "\u2a8f",
+    "lsqb;": "[",
+    "lsquo;": "\u2018",
+    "lsquor;": "\u201a",
+    "Lstrok;": "\u0141",
+    "lstrok;": "\u0142",
+    "LT": "<",
+    "lt": "<",
+    "LT;": "<",
+    "Lt;": "\u226a",
+    "lt;": "<",
+    "ltcc;": "\u2aa6",
+    "ltcir;": "\u2a79",
+    "ltdot;": "\u22d6",
+    "lthree;": "\u22cb",
+    "ltimes;": "\u22c9",
+    "ltlarr;": "\u2976",
+    "ltquest;": "\u2a7b",
+    "ltri;": "\u25c3",
+    "ltrie;": "\u22b4",
+    "ltrif;": "\u25c2",
+    "ltrPar;": "\u2996",
+    "lurdshar;": "\u294a",
+    "luruhar;": "\u2966",
+    "lvertneqq;": "\u2268\ufe00",
+    "lvnE;": "\u2268\ufe00",
+    "macr": "\xaf",
+    "macr;": "\xaf",
+    "male;": "\u2642",
+    "malt;": "\u2720",
+    "maltese;": "\u2720",
+    "Map;": "\u2905",
+    "map;": "\u21a6",
+    "mapsto;": "\u21a6",
+    "mapstodown;": "\u21a7",
+    "mapstoleft;": "\u21a4",
+    "mapstoup;": "\u21a5",
+    "marker;": "\u25ae",
+    "mcomma;": "\u2a29",
+    "Mcy;": "\u041c",
+    "mcy;": "\u043c",
+    "mdash;": "\u2014",
+    "mDDot;": "\u223a",
+    "measuredangle;": "\u2221",
+    "MediumSpace;": "\u205f",
+    "Mellintrf;": "\u2133",
+    "Mfr;": "\U0001d510",
+    "mfr;": "\U0001d52a",
+    "mho;": "\u2127",
+    "micro": "\xb5",
+    "micro;": "\xb5",
+    "mid;": "\u2223",
+    "midast;": "*",
+    "midcir;": "\u2af0",
+    "middot": "\xb7",
+    "middot;": "\xb7",
+    "minus;": "\u2212",
+    "minusb;": "\u229f",
+    "minusd;": "\u2238",
+    "minusdu;": "\u2a2a",
+    "MinusPlus;": "\u2213",
+    "mlcp;": "\u2adb",
+    "mldr;": "\u2026",
+    "mnplus;": "\u2213",
+    "models;": "\u22a7",
+    "Mopf;": "\U0001d544",
+    "mopf;": "\U0001d55e",
+    "mp;": "\u2213",
+    "Mscr;": "\u2133",
+    "mscr;": "\U0001d4c2",
+    "mstpos;": "\u223e",
+    "Mu;": "\u039c",
+    "mu;": "\u03bc",
+    "multimap;": "\u22b8",
+    "mumap;": "\u22b8",
+    "nabla;": "\u2207",
+    "Nacute;": "\u0143",
+    "nacute;": "\u0144",
+    "nang;": "\u2220\u20d2",
+    "nap;": "\u2249",
+    "napE;": "\u2a70\u0338",
+    "napid;": "\u224b\u0338",
+    "napos;": "\u0149",
+    "napprox;": "\u2249",
+    "natur;": "\u266e",
+    "natural;": "\u266e",
+    "naturals;": "\u2115",
+    "nbsp": "\xa0",
+    "nbsp;": "\xa0",
+    "nbump;": "\u224e\u0338",
+    "nbumpe;": "\u224f\u0338",
+    "ncap;": "\u2a43",
+    "Ncaron;": "\u0147",
+    "ncaron;": "\u0148",
+    "Ncedil;": "\u0145",
+    "ncedil;": "\u0146",
+    "ncong;": "\u2247",
+    "ncongdot;": "\u2a6d\u0338",
+    "ncup;": "\u2a42",
+    "Ncy;": "\u041d",
+    "ncy;": "\u043d",
+    "ndash;": "\u2013",
+    "ne;": "\u2260",
+    "nearhk;": "\u2924",
+    "neArr;": "\u21d7",
+    "nearr;": "\u2197",
+    "nearrow;": "\u2197",
+    "nedot;": "\u2250\u0338",
+    "NegativeMediumSpace;": "\u200b",
+    "NegativeThickSpace;": "\u200b",
+    "NegativeThinSpace;": "\u200b",
+    "NegativeVeryThinSpace;": "\u200b",
+    "nequiv;": "\u2262",
+    "nesear;": "\u2928",
+    "nesim;": "\u2242\u0338",
+    "NestedGreaterGreater;": "\u226b",
+    "NestedLessLess;": "\u226a",
+    "NewLine;": "\n",
+    "nexist;": "\u2204",
+    "nexists;": "\u2204",
+    "Nfr;": "\U0001d511",
+    "nfr;": "\U0001d52b",
+    "ngE;": "\u2267\u0338",
+    "nge;": "\u2271",
+    "ngeq;": "\u2271",
+    "ngeqq;": "\u2267\u0338",
+    "ngeqslant;": "\u2a7e\u0338",
+    "nges;": "\u2a7e\u0338",
+    "nGg;": "\u22d9\u0338",
+    "ngsim;": "\u2275",
+    "nGt;": "\u226b\u20d2",
+    "ngt;": "\u226f",
+    "ngtr;": "\u226f",
+    "nGtv;": "\u226b\u0338",
+    "nhArr;": "\u21ce",
+    "nharr;": "\u21ae",
+    "nhpar;": "\u2af2",
+    "ni;": "\u220b",
+    "nis;": "\u22fc",
+    "nisd;": "\u22fa",
+    "niv;": "\u220b",
+    "NJcy;": "\u040a",
+    "njcy;": "\u045a",
+    "nlArr;": "\u21cd",
+    "nlarr;": "\u219a",
+    "nldr;": "\u2025",
+    "nlE;": "\u2266\u0338",
+    "nle;": "\u2270",
+    "nLeftarrow;": "\u21cd",
+    "nleftarrow;": "\u219a",
+    "nLeftrightarrow;": "\u21ce",
+    "nleftrightarrow;": "\u21ae",
+    "nleq;": "\u2270",
+    "nleqq;": "\u2266\u0338",
+    "nleqslant;": "\u2a7d\u0338",
+    "nles;": "\u2a7d\u0338",
+    "nless;": "\u226e",
+    "nLl;": "\u22d8\u0338",
+    "nlsim;": "\u2274",
+    "nLt;": "\u226a\u20d2",
+    "nlt;": "\u226e",
+    "nltri;": "\u22ea",
+    "nltrie;": "\u22ec",
+    "nLtv;": "\u226a\u0338",
+    "nmid;": "\u2224",
+    "NoBreak;": "\u2060",
+    "NonBreakingSpace;": "\xa0",
+    "Nopf;": "\u2115",
+    "nopf;": "\U0001d55f",
+    "not": "\xac",
+    "Not;": "\u2aec",
+    "not;": "\xac",
+    "NotCongruent;": "\u2262",
+    "NotCupCap;": "\u226d",
+    "NotDoubleVerticalBar;": "\u2226",
+    "NotElement;": "\u2209",
+    "NotEqual;": "\u2260",
+    "NotEqualTilde;": "\u2242\u0338",
+    "NotExists;": "\u2204",
+    "NotGreater;": "\u226f",
+    "NotGreaterEqual;": "\u2271",
+    "NotGreaterFullEqual;": "\u2267\u0338",
+    "NotGreaterGreater;": "\u226b\u0338",
+    "NotGreaterLess;": "\u2279",
+    "NotGreaterSlantEqual;": "\u2a7e\u0338",
+    "NotGreaterTilde;": "\u2275",
+    "NotHumpDownHump;": "\u224e\u0338",
+    "NotHumpEqual;": "\u224f\u0338",
+    "notin;": "\u2209",
+    "notindot;": "\u22f5\u0338",
+    "notinE;": "\u22f9\u0338",
+    "notinva;": "\u2209",
+    "notinvb;": "\u22f7",
+    "notinvc;": "\u22f6",
+    "NotLeftTriangle;": "\u22ea",
+    "NotLeftTriangleBar;": "\u29cf\u0338",
+    "NotLeftTriangleEqual;": "\u22ec",
+    "NotLess;": "\u226e",
+    "NotLessEqual;": "\u2270",
+    "NotLessGreater;": "\u2278",
+    "NotLessLess;": "\u226a\u0338",
+    "NotLessSlantEqual;": "\u2a7d\u0338",
+    "NotLessTilde;": "\u2274",
+    "NotNestedGreaterGreater;": "\u2aa2\u0338",
+    "NotNestedLessLess;": "\u2aa1\u0338",
+    "notni;": "\u220c",
+    "notniva;": "\u220c",
+    "notnivb;": "\u22fe",
+    "notnivc;": "\u22fd",
+    "NotPrecedes;": "\u2280",
+    "NotPrecedesEqual;": "\u2aaf\u0338",
+    "NotPrecedesSlantEqual;": "\u22e0",
+    "NotReverseElement;": "\u220c",
+    "NotRightTriangle;": "\u22eb",
+    "NotRightTriangleBar;": "\u29d0\u0338",
+    "NotRightTriangleEqual;": "\u22ed",
+    "NotSquareSubset;": "\u228f\u0338",
+    "NotSquareSubsetEqual;": "\u22e2",
+    "NotSquareSuperset;": "\u2290\u0338",
+    "NotSquareSupersetEqual;": "\u22e3",
+    "NotSubset;": "\u2282\u20d2",
+    "NotSubsetEqual;": "\u2288",
+    "NotSucceeds;": "\u2281",
+    "NotSucceedsEqual;": "\u2ab0\u0338",
+    "NotSucceedsSlantEqual;": "\u22e1",
+    "NotSucceedsTilde;": "\u227f\u0338",
+    "NotSuperset;": "\u2283\u20d2",
+    "NotSupersetEqual;": "\u2289",
+    "NotTilde;": "\u2241",
+    "NotTildeEqual;": "\u2244",
+    "NotTildeFullEqual;": "\u2247",
+    "NotTildeTilde;": "\u2249",
+    "NotVerticalBar;": "\u2224",
+    "npar;": "\u2226",
+    "nparallel;": "\u2226",
+    "nparsl;": "\u2afd\u20e5",
+    "npart;": "\u2202\u0338",
+    "npolint;": "\u2a14",
+    "npr;": "\u2280",
+    "nprcue;": "\u22e0",
+    "npre;": "\u2aaf\u0338",
+    "nprec;": "\u2280",
+    "npreceq;": "\u2aaf\u0338",
+    "nrArr;": "\u21cf",
+    "nrarr;": "\u219b",
+    "nrarrc;": "\u2933\u0338",
+    "nrarrw;": "\u219d\u0338",
+    "nRightarrow;": "\u21cf",
+    "nrightarrow;": "\u219b",
+    "nrtri;": "\u22eb",
+    "nrtrie;": "\u22ed",
+    "nsc;": "\u2281",
+    "nsccue;": "\u22e1",
+    "nsce;": "\u2ab0\u0338",
+    "Nscr;": "\U0001d4a9",
+    "nscr;": "\U0001d4c3",
+    "nshortmid;": "\u2224",
+    "nshortparallel;": "\u2226",
+    "nsim;": "\u2241",
+    "nsime;": "\u2244",
+    "nsimeq;": "\u2244",
+    "nsmid;": "\u2224",
+    "nspar;": "\u2226",
+    "nsqsube;": "\u22e2",
+    "nsqsupe;": "\u22e3",
+    "nsub;": "\u2284",
+    "nsubE;": "\u2ac5\u0338",
+    "nsube;": "\u2288",
+    "nsubset;": "\u2282\u20d2",
+    "nsubseteq;": "\u2288",
+    "nsubseteqq;": "\u2ac5\u0338",
+    "nsucc;": "\u2281",
+    "nsucceq;": "\u2ab0\u0338",
+    "nsup;": "\u2285",
+    "nsupE;": "\u2ac6\u0338",
+    "nsupe;": "\u2289",
+    "nsupset;": "\u2283\u20d2",
+    "nsupseteq;": "\u2289",
+    "nsupseteqq;": "\u2ac6\u0338",
+    "ntgl;": "\u2279",
+    "Ntilde": "\xd1",
+    "ntilde": "\xf1",
+    "Ntilde;": "\xd1",
+    "ntilde;": "\xf1",
+    "ntlg;": "\u2278",
+    "ntriangleleft;": "\u22ea",
+    "ntrianglelefteq;": "\u22ec",
+    "ntriangleright;": "\u22eb",
+    "ntrianglerighteq;": "\u22ed",
+    "Nu;": "\u039d",
+    "nu;": "\u03bd",
+    "num;": "#",
+    "numero;": "\u2116",
+    "numsp;": "\u2007",
+    "nvap;": "\u224d\u20d2",
+    "nVDash;": "\u22af",
+    "nVdash;": "\u22ae",
+    "nvDash;": "\u22ad",
+    "nvdash;": "\u22ac",
+    "nvge;": "\u2265\u20d2",
+    "nvgt;": ">\u20d2",
+    "nvHarr;": "\u2904",
+    "nvinfin;": "\u29de",
+    "nvlArr;": "\u2902",
+    "nvle;": "\u2264\u20d2",
+    "nvlt;": "<\u20d2",
+    "nvltrie;": "\u22b4\u20d2",
+    "nvrArr;": "\u2903",
+    "nvrtrie;": "\u22b5\u20d2",
+    "nvsim;": "\u223c\u20d2",
+    "nwarhk;": "\u2923",
+    "nwArr;": "\u21d6",
+    "nwarr;": "\u2196",
+    "nwarrow;": "\u2196",
+    "nwnear;": "\u2927",
+    "Oacute": "\xd3",
+    "oacute": "\xf3",
+    "Oacute;": "\xd3",
+    "oacute;": "\xf3",
+    "oast;": "\u229b",
+    "ocir;": "\u229a",
+    "Ocirc": "\xd4",
+    "ocirc": "\xf4",
+    "Ocirc;": "\xd4",
+    "ocirc;": "\xf4",
+    "Ocy;": "\u041e",
+    "ocy;": "\u043e",
+    "odash;": "\u229d",
+    "Odblac;": "\u0150",
+    "odblac;": "\u0151",
+    "odiv;": "\u2a38",
+    "odot;": "\u2299",
+    "odsold;": "\u29bc",
+    "OElig;": "\u0152",
+    "oelig;": "\u0153",
+    "ofcir;": "\u29bf",
+    "Ofr;": "\U0001d512",
+    "ofr;": "\U0001d52c",
+    "ogon;": "\u02db",
+    "Ograve": "\xd2",
+    "ograve": "\xf2",
+    "Ograve;": "\xd2",
+    "ograve;": "\xf2",
+    "ogt;": "\u29c1",
+    "ohbar;": "\u29b5",
+    "ohm;": "\u03a9",
+    "oint;": "\u222e",
+    "olarr;": "\u21ba",
+    "olcir;": "\u29be",
+    "olcross;": "\u29bb",
+    "oline;": "\u203e",
+    "olt;": "\u29c0",
+    "Omacr;": "\u014c",
+    "omacr;": "\u014d",
+    "Omega;": "\u03a9",
+    "omega;": "\u03c9",
+    "Omicron;": "\u039f",
+    "omicron;": "\u03bf",
+    "omid;": "\u29b6",
+    "ominus;": "\u2296",
+    "Oopf;": "\U0001d546",
+    "oopf;": "\U0001d560",
+    "opar;": "\u29b7",
+    "OpenCurlyDoubleQuote;": "\u201c",
+    "OpenCurlyQuote;": "\u2018",
+    "operp;": "\u29b9",
+    "oplus;": "\u2295",
+    "Or;": "\u2a54",
+    "or;": "\u2228",
+    "orarr;": "\u21bb",
+    "ord;": "\u2a5d",
+    "order;": "\u2134",
+    "orderof;": "\u2134",
+    "ordf": "\xaa",
+    "ordf;": "\xaa",
+    "ordm": "\xba",
+    "ordm;": "\xba",
+    "origof;": "\u22b6",
+    "oror;": "\u2a56",
+    "orslope;": "\u2a57",
+    "orv;": "\u2a5b",
+    "oS;": "\u24c8",
+    "Oscr;": "\U0001d4aa",
+    "oscr;": "\u2134",
+    "Oslash": "\xd8",
+    "oslash": "\xf8",
+    "Oslash;": "\xd8",
+    "oslash;": "\xf8",
+    "osol;": "\u2298",
+    "Otilde": "\xd5",
+    "otilde": "\xf5",
+    "Otilde;": "\xd5",
+    "otilde;": "\xf5",
+    "Otimes;": "\u2a37",
+    "otimes;": "\u2297",
+    "otimesas;": "\u2a36",
+    "Ouml": "\xd6",
+    "ouml": "\xf6",
+    "Ouml;": "\xd6",
+    "ouml;": "\xf6",
+    "ovbar;": "\u233d",
+    "OverBar;": "\u203e",
+    "OverBrace;": "\u23de",
+    "OverBracket;": "\u23b4",
+    "OverParenthesis;": "\u23dc",
+    "par;": "\u2225",
+    "para": "\xb6",
+    "para;": "\xb6",
+    "parallel;": "\u2225",
+    "parsim;": "\u2af3",
+    "parsl;": "\u2afd",
+    "part;": "\u2202",
+    "PartialD;": "\u2202",
+    "Pcy;": "\u041f",
+    "pcy;": "\u043f",
+    "percnt;": "%",
+    "period;": ".",
+    "permil;": "\u2030",
+    "perp;": "\u22a5",
+    "pertenk;": "\u2031",
+    "Pfr;": "\U0001d513",
+    "pfr;": "\U0001d52d",
+    "Phi;": "\u03a6",
+    "phi;": "\u03c6",
+    "phiv;": "\u03d5",
+    "phmmat;": "\u2133",
+    "phone;": "\u260e",
+    "Pi;": "\u03a0",
+    "pi;": "\u03c0",
+    "pitchfork;": "\u22d4",
+    "piv;": "\u03d6",
+    "planck;": "\u210f",
+    "planckh;": "\u210e",
+    "plankv;": "\u210f",
+    "plus;": "+",
+    "plusacir;": "\u2a23",
+    "plusb;": "\u229e",
+    "pluscir;": "\u2a22",
+    "plusdo;": "\u2214",
+    "plusdu;": "\u2a25",
+    "pluse;": "\u2a72",
+    "PlusMinus;": "\xb1",
+    "plusmn": "\xb1",
+    "plusmn;": "\xb1",
+    "plussim;": "\u2a26",
+    "plustwo;": "\u2a27",
+    "pm;": "\xb1",
+    "Poincareplane;": "\u210c",
+    "pointint;": "\u2a15",
+    "Popf;": "\u2119",
+    "popf;": "\U0001d561",
+    "pound": "\xa3",
+    "pound;": "\xa3",
+    "Pr;": "\u2abb",
+    "pr;": "\u227a",
+    "prap;": "\u2ab7",
+    "prcue;": "\u227c",
+    "prE;": "\u2ab3",
+    "pre;": "\u2aaf",
+    "prec;": "\u227a",
+    "precapprox;": "\u2ab7",
+    "preccurlyeq;": "\u227c",
+    "Precedes;": "\u227a",
+    "PrecedesEqual;": "\u2aaf",
+    "PrecedesSlantEqual;": "\u227c",
+    "PrecedesTilde;": "\u227e",
+    "preceq;": "\u2aaf",
+    "precnapprox;": "\u2ab9",
+    "precneqq;": "\u2ab5",
+    "precnsim;": "\u22e8",
+    "precsim;": "\u227e",
+    "Prime;": "\u2033",
+    "prime;": "\u2032",
+    "primes;": "\u2119",
+    "prnap;": "\u2ab9",
+    "prnE;": "\u2ab5",
+    "prnsim;": "\u22e8",
+    "prod;": "\u220f",
+    "Product;": "\u220f",
+    "profalar;": "\u232e",
+    "profline;": "\u2312",
+    "profsurf;": "\u2313",
+    "prop;": "\u221d",
+    "Proportion;": "\u2237",
+    "Proportional;": "\u221d",
+    "propto;": "\u221d",
+    "prsim;": "\u227e",
+    "prurel;": "\u22b0",
+    "Pscr;": "\U0001d4ab",
+    "pscr;": "\U0001d4c5",
+    "Psi;": "\u03a8",
+    "psi;": "\u03c8",
+    "puncsp;": "\u2008",
+    "Qfr;": "\U0001d514",
+    "qfr;": "\U0001d52e",
+    "qint;": "\u2a0c",
+    "Qopf;": "\u211a",
+    "qopf;": "\U0001d562",
+    "qprime;": "\u2057",
+    "Qscr;": "\U0001d4ac",
+    "qscr;": "\U0001d4c6",
+    "quaternions;": "\u210d",
+    "quatint;": "\u2a16",
+    "quest;": "?",
+    "questeq;": "\u225f",
+    "QUOT": '"',
+    "quot": '"',
+    "QUOT;": '"',
+    "quot;": '"',
+    "rAarr;": "\u21db",
+    "race;": "\u223d\u0331",
+    "Racute;": "\u0154",
+    "racute;": "\u0155",
+    "radic;": "\u221a",
+    "raemptyv;": "\u29b3",
+    "Rang;": "\u27eb",
+    "rang;": "\u27e9",
+    "rangd;": "\u2992",
+    "range;": "\u29a5",
+    "rangle;": "\u27e9",
+    "raquo": "\xbb",
+    "raquo;": "\xbb",
+    "Rarr;": "\u21a0",
+    "rArr;": "\u21d2",
+    "rarr;": "\u2192",
+    "rarrap;": "\u2975",
+    "rarrb;": "\u21e5",
+    "rarrbfs;": "\u2920",
+    "rarrc;": "\u2933",
+    "rarrfs;": "\u291e",
+    "rarrhk;": "\u21aa",
+    "rarrlp;": "\u21ac",
+    "rarrpl;": "\u2945",
+    "rarrsim;": "\u2974",
+    "Rarrtl;": "\u2916",
+    "rarrtl;": "\u21a3",
+    "rarrw;": "\u219d",
+    "rAtail;": "\u291c",
+    "ratail;": "\u291a",
+    "ratio;": "\u2236",
+    "rationals;": "\u211a",
+    "RBarr;": "\u2910",
+    "rBarr;": "\u290f",
+    "rbarr;": "\u290d",
+    "rbbrk;": "\u2773",
+    "rbrace;": "}",
+    "rbrack;": "]",
+    "rbrke;": "\u298c",
+    "rbrksld;": "\u298e",
+    "rbrkslu;": "\u2990",
+    "Rcaron;": "\u0158",
+    "rcaron;": "\u0159",
+    "Rcedil;": "\u0156",
+    "rcedil;": "\u0157",
+    "rceil;": "\u2309",
+    "rcub;": "}",
+    "Rcy;": "\u0420",
+    "rcy;": "\u0440",
+    "rdca;": "\u2937",
+    "rdldhar;": "\u2969",
+    "rdquo;": "\u201d",
+    "rdquor;": "\u201d",
+    "rdsh;": "\u21b3",
+    "Re;": "\u211c",
+    "real;": "\u211c",
+    "realine;": "\u211b",
+    "realpart;": "\u211c",
+    "reals;": "\u211d",
+    "rect;": "\u25ad",
+    "REG": "\xae",
+    "reg": "\xae",
+    "REG;": "\xae",
+    "reg;": "\xae",
+    "ReverseElement;": "\u220b",
+    "ReverseEquilibrium;": "\u21cb",
+    "ReverseUpEquilibrium;": "\u296f",
+    "rfisht;": "\u297d",
+    "rfloor;": "\u230b",
+    "Rfr;": "\u211c",
+    "rfr;": "\U0001d52f",
+    "rHar;": "\u2964",
+    "rhard;": "\u21c1",
+    "rharu;": "\u21c0",
+    "rharul;": "\u296c",
+    "Rho;": "\u03a1",
+    "rho;": "\u03c1",
+    "rhov;": "\u03f1",
+    "RightAngleBracket;": "\u27e9",
+    "RightArrow;": "\u2192",
+    "Rightarrow;": "\u21d2",
+    "rightarrow;": "\u2192",
+    "RightArrowBar;": "\u21e5",
+    "RightArrowLeftArrow;": "\u21c4",
+    "rightarrowtail;": "\u21a3",
+    "RightCeiling;": "\u2309",
+    "RightDoubleBracket;": "\u27e7",
+    "RightDownTeeVector;": "\u295d",
+    "RightDownVector;": "\u21c2",
+    "RightDownVectorBar;": "\u2955",
+    "RightFloor;": "\u230b",
+    "rightharpoondown;": "\u21c1",
+    "rightharpoonup;": "\u21c0",
+    "rightleftarrows;": "\u21c4",
+    "rightleftharpoons;": "\u21cc",
+    "rightrightarrows;": "\u21c9",
+    "rightsquigarrow;": "\u219d",
+    "RightTee;": "\u22a2",
+    "RightTeeArrow;": "\u21a6",
+    "RightTeeVector;": "\u295b",
+    "rightthreetimes;": "\u22cc",
+    "RightTriangle;": "\u22b3",
+    "RightTriangleBar;": "\u29d0",
+    "RightTriangleEqual;": "\u22b5",
+    "RightUpDownVector;": "\u294f",
+    "RightUpTeeVector;": "\u295c",
+    "RightUpVector;": "\u21be",
+    "RightUpVectorBar;": "\u2954",
+    "RightVector;": "\u21c0",
+    "RightVectorBar;": "\u2953",
+    "ring;": "\u02da",
+    "risingdotseq;": "\u2253",
+    "rlarr;": "\u21c4",
+    "rlhar;": "\u21cc",
+    "rlm;": "\u200f",
+    "rmoust;": "\u23b1",
+    "rmoustache;": "\u23b1",
+    "rnmid;": "\u2aee",
+    "roang;": "\u27ed",
+    "roarr;": "\u21fe",
+    "robrk;": "\u27e7",
+    "ropar;": "\u2986",
+    "Ropf;": "\u211d",
+    "ropf;": "\U0001d563",
+    "roplus;": "\u2a2e",
+    "rotimes;": "\u2a35",
+    "RoundImplies;": "\u2970",
+    "rpar;": ")",
+    "rpargt;": "\u2994",
+    "rppolint;": "\u2a12",
+    "rrarr;": "\u21c9",
+    "Rrightarrow;": "\u21db",
+    "rsaquo;": "\u203a",
+    "Rscr;": "\u211b",
+    "rscr;": "\U0001d4c7",
+    "Rsh;": "\u21b1",
+    "rsh;": "\u21b1",
+    "rsqb;": "]",
+    "rsquo;": "\u2019",
+    "rsquor;": "\u2019",
+    "rthree;": "\u22cc",
+    "rtimes;": "\u22ca",
+    "rtri;": "\u25b9",
+    "rtrie;": "\u22b5",
+    "rtrif;": "\u25b8",
+    "rtriltri;": "\u29ce",
+    "RuleDelayed;": "\u29f4",
+    "ruluhar;": "\u2968",
+    "rx;": "\u211e",
+    "Sacute;": "\u015a",
+    "sacute;": "\u015b",
+    "sbquo;": "\u201a",
+    "Sc;": "\u2abc",
+    "sc;": "\u227b",
+    "scap;": "\u2ab8",
+    "Scaron;": "\u0160",
+    "scaron;": "\u0161",
+    "sccue;": "\u227d",
+    "scE;": "\u2ab4",
+    "sce;": "\u2ab0",
+    "Scedil;": "\u015e",
+    "scedil;": "\u015f",
+    "Scirc;": "\u015c",
+    "scirc;": "\u015d",
+    "scnap;": "\u2aba",
+    "scnE;": "\u2ab6",
+    "scnsim;": "\u22e9",
+    "scpolint;": "\u2a13",
+    "scsim;": "\u227f",
+    "Scy;": "\u0421",
+    "scy;": "\u0441",
+    "sdot;": "\u22c5",
+    "sdotb;": "\u22a1",
+    "sdote;": "\u2a66",
+    "searhk;": "\u2925",
+    "seArr;": "\u21d8",
+    "searr;": "\u2198",
+    "searrow;": "\u2198",
+    "sect": "\xa7",
+    "sect;": "\xa7",
+    "semi;": ";",
+    "seswar;": "\u2929",
+    "setminus;": "\u2216",
+    "setmn;": "\u2216",
+    "sext;": "\u2736",
+    "Sfr;": "\U0001d516",
+    "sfr;": "\U0001d530",
+    "sfrown;": "\u2322",
+    "sharp;": "\u266f",
+    "SHCHcy;": "\u0429",
+    "shchcy;": "\u0449",
+    "SHcy;": "\u0428",
+    "shcy;": "\u0448",
+    "ShortDownArrow;": "\u2193",
+    "ShortLeftArrow;": "\u2190",
+    "shortmid;": "\u2223",
+    "shortparallel;": "\u2225",
+    "ShortRightArrow;": "\u2192",
+    "ShortUpArrow;": "\u2191",
+    "shy": "\xad",
+    "shy;": "\xad",
+    "Sigma;": "\u03a3",
+    "sigma;": "\u03c3",
+    "sigmaf;": "\u03c2",
+    "sigmav;": "\u03c2",
+    "sim;": "\u223c",
+    "simdot;": "\u2a6a",
+    "sime;": "\u2243",
+    "simeq;": "\u2243",
+    "simg;": "\u2a9e",
+    "simgE;": "\u2aa0",
+    "siml;": "\u2a9d",
+    "simlE;": "\u2a9f",
+    "simne;": "\u2246",
+    "simplus;": "\u2a24",
+    "simrarr;": "\u2972",
+    "slarr;": "\u2190",
+    "SmallCircle;": "\u2218",
+    "smallsetminus;": "\u2216",
+    "smashp;": "\u2a33",
+    "smeparsl;": "\u29e4",
+    "smid;": "\u2223",
+    "smile;": "\u2323",
+    "smt;": "\u2aaa",
+    "smte;": "\u2aac",
+    "smtes;": "\u2aac\ufe00",
+    "SOFTcy;": "\u042c",
+    "softcy;": "\u044c",
+    "sol;": "/",
+    "solb;": "\u29c4",
+    "solbar;": "\u233f",
+    "Sopf;": "\U0001d54a",
+    "sopf;": "\U0001d564",
+    "spades;": "\u2660",
+    "spadesuit;": "\u2660",
+    "spar;": "\u2225",
+    "sqcap;": "\u2293",
+    "sqcaps;": "\u2293\ufe00",
+    "sqcup;": "\u2294",
+    "sqcups;": "\u2294\ufe00",
+    "Sqrt;": "\u221a",
+    "sqsub;": "\u228f",
+    "sqsube;": "\u2291",
+    "sqsubset;": "\u228f",
+    "sqsubseteq;": "\u2291",
+    "sqsup;": "\u2290",
+    "sqsupe;": "\u2292",
+    "sqsupset;": "\u2290",
+    "sqsupseteq;": "\u2292",
+    "squ;": "\u25a1",
+    "Square;": "\u25a1",
+    "square;": "\u25a1",
+    "SquareIntersection;": "\u2293",
+    "SquareSubset;": "\u228f",
+    "SquareSubsetEqual;": "\u2291",
+    "SquareSuperset;": "\u2290",
+    "SquareSupersetEqual;": "\u2292",
+    "SquareUnion;": "\u2294",
+    "squarf;": "\u25aa",
+    "squf;": "\u25aa",
+    "srarr;": "\u2192",
+    "Sscr;": "\U0001d4ae",
+    "sscr;": "\U0001d4c8",
+    "ssetmn;": "\u2216",
+    "ssmile;": "\u2323",
+    "sstarf;": "\u22c6",
+    "Star;": "\u22c6",
+    "star;": "\u2606",
+    "starf;": "\u2605",
+    "straightepsilon;": "\u03f5",
+    "straightphi;": "\u03d5",
+    "strns;": "\xaf",
+    "Sub;": "\u22d0",
+    "sub;": "\u2282",
+    "subdot;": "\u2abd",
+    "subE;": "\u2ac5",
+    "sube;": "\u2286",
+    "subedot;": "\u2ac3",
+    "submult;": "\u2ac1",
+    "subnE;": "\u2acb",
+    "subne;": "\u228a",
+    "subplus;": "\u2abf",
+    "subrarr;": "\u2979",
+    "Subset;": "\u22d0",
+    "subset;": "\u2282",
+    "subseteq;": "\u2286",
+    "subseteqq;": "\u2ac5",
+    "SubsetEqual;": "\u2286",
+    "subsetneq;": "\u228a",
+    "subsetneqq;": "\u2acb",
+    "subsim;": "\u2ac7",
+    "subsub;": "\u2ad5",
+    "subsup;": "\u2ad3",
+    "succ;": "\u227b",
+    "succapprox;": "\u2ab8",
+    "succcurlyeq;": "\u227d",
+    "Succeeds;": "\u227b",
+    "SucceedsEqual;": "\u2ab0",
+    "SucceedsSlantEqual;": "\u227d",
+    "SucceedsTilde;": "\u227f",
+    "succeq;": "\u2ab0",
+    "succnapprox;": "\u2aba",
+    "succneqq;": "\u2ab6",
+    "succnsim;": "\u22e9",
+    "succsim;": "\u227f",
+    "SuchThat;": "\u220b",
+    "Sum;": "\u2211",
+    "sum;": "\u2211",
+    "sung;": "\u266a",
+    "sup1": "\xb9",
+    "sup1;": "\xb9",
+    "sup2": "\xb2",
+    "sup2;": "\xb2",
+    "sup3": "\xb3",
+    "sup3;": "\xb3",
+    "Sup;": "\u22d1",
+    "sup;": "\u2283",
+    "supdot;": "\u2abe",
+    "supdsub;": "\u2ad8",
+    "supE;": "\u2ac6",
+    "supe;": "\u2287",
+    "supedot;": "\u2ac4",
+    "Superset;": "\u2283",
+    "SupersetEqual;": "\u2287",
+    "suphsol;": "\u27c9",
+    "suphsub;": "\u2ad7",
+    "suplarr;": "\u297b",
+    "supmult;": "\u2ac2",
+    "supnE;": "\u2acc",
+    "supne;": "\u228b",
+    "supplus;": "\u2ac0",
+    "Supset;": "\u22d1",
+    "supset;": "\u2283",
+    "supseteq;": "\u2287",
+    "supseteqq;": "\u2ac6",
+    "supsetneq;": "\u228b",
+    "supsetneqq;": "\u2acc",
+    "supsim;": "\u2ac8",
+    "supsub;": "\u2ad4",
+    "supsup;": "\u2ad6",
+    "swarhk;": "\u2926",
+    "swArr;": "\u21d9",
+    "swarr;": "\u2199",
+    "swarrow;": "\u2199",
+    "swnwar;": "\u292a",
+    "szlig": "\xdf",
+    "szlig;": "\xdf",
+    "Tab;": "\t",
+    "target;": "\u2316",
+    "Tau;": "\u03a4",
+    "tau;": "\u03c4",
+    "tbrk;": "\u23b4",
+    "Tcaron;": "\u0164",
+    "tcaron;": "\u0165",
+    "Tcedil;": "\u0162",
+    "tcedil;": "\u0163",
+    "Tcy;": "\u0422",
+    "tcy;": "\u0442",
+    "tdot;": "\u20db",
+    "telrec;": "\u2315",
+    "Tfr;": "\U0001d517",
+    "tfr;": "\U0001d531",
+    "there4;": "\u2234",
+    "Therefore;": "\u2234",
+    "therefore;": "\u2234",
+    "Theta;": "\u0398",
+    "theta;": "\u03b8",
+    "thetasym;": "\u03d1",
+    "thetav;": "\u03d1",
+    "thickapprox;": "\u2248",
+    "thicksim;": "\u223c",
+    "ThickSpace;": "\u205f\u200a",
+    "thinsp;": "\u2009",
+    "ThinSpace;": "\u2009",
+    "thkap;": "\u2248",
+    "thksim;": "\u223c",
+    "THORN": "\xde",
+    "thorn": "\xfe",
+    "THORN;": "\xde",
+    "thorn;": "\xfe",
+    "Tilde;": "\u223c",
+    "tilde;": "\u02dc",
+    "TildeEqual;": "\u2243",
+    "TildeFullEqual;": "\u2245",
+    "TildeTilde;": "\u2248",
+    "times": "\xd7",
+    "times;": "\xd7",
+    "timesb;": "\u22a0",
+    "timesbar;": "\u2a31",
+    "timesd;": "\u2a30",
+    "tint;": "\u222d",
+    "toea;": "\u2928",
+    "top;": "\u22a4",
+    "topbot;": "\u2336",
+    "topcir;": "\u2af1",
+    "Topf;": "\U0001d54b",
+    "topf;": "\U0001d565",
+    "topfork;": "\u2ada",
+    "tosa;": "\u2929",
+    "tprime;": "\u2034",
+    "TRADE;": "\u2122",
+    "trade;": "\u2122",
+    "triangle;": "\u25b5",
+    "triangledown;": "\u25bf",
+    "triangleleft;": "\u25c3",
+    "trianglelefteq;": "\u22b4",
+    "triangleq;": "\u225c",
+    "triangleright;": "\u25b9",
+    "trianglerighteq;": "\u22b5",
+    "tridot;": "\u25ec",
+    "trie;": "\u225c",
+    "triminus;": "\u2a3a",
+    "TripleDot;": "\u20db",
+    "triplus;": "\u2a39",
+    "trisb;": "\u29cd",
+    "tritime;": "\u2a3b",
+    "trpezium;": "\u23e2",
+    "Tscr;": "\U0001d4af",
+    "tscr;": "\U0001d4c9",
+    "TScy;": "\u0426",
+    "tscy;": "\u0446",
+    "TSHcy;": "\u040b",
+    "tshcy;": "\u045b",
+    "Tstrok;": "\u0166",
+    "tstrok;": "\u0167",
+    "twixt;": "\u226c",
+    "twoheadleftarrow;": "\u219e",
+    "twoheadrightarrow;": "\u21a0",
+    "Uacute": "\xda",
+    "uacute": "\xfa",
+    "Uacute;": "\xda",
+    "uacute;": "\xfa",
+    "Uarr;": "\u219f",
+    "uArr;": "\u21d1",
+    "uarr;": "\u2191",
+    "Uarrocir;": "\u2949",
+    "Ubrcy;": "\u040e",
+    "ubrcy;": "\u045e",
+    "Ubreve;": "\u016c",
+    "ubreve;": "\u016d",
+    "Ucirc": "\xdb",
+    "ucirc": "\xfb",
+    "Ucirc;": "\xdb",
+    "ucirc;": "\xfb",
+    "Ucy;": "\u0423",
+    "ucy;": "\u0443",
+    "udarr;": "\u21c5",
+    "Udblac;": "\u0170",
+    "udblac;": "\u0171",
+    "udhar;": "\u296e",
+    "ufisht;": "\u297e",
+    "Ufr;": "\U0001d518",
+    "ufr;": "\U0001d532",
+    "Ugrave": "\xd9",
+    "ugrave": "\xf9",
+    "Ugrave;": "\xd9",
+    "ugrave;": "\xf9",
+    "uHar;": "\u2963",
+    "uharl;": "\u21bf",
+    "uharr;": "\u21be",
+    "uhblk;": "\u2580",
+    "ulcorn;": "\u231c",
+    "ulcorner;": "\u231c",
+    "ulcrop;": "\u230f",
+    "ultri;": "\u25f8",
+    "Umacr;": "\u016a",
+    "umacr;": "\u016b",
+    "uml": "\xa8",
+    "uml;": "\xa8",
+    "UnderBar;": "_",
+    "UnderBrace;": "\u23df",
+    "UnderBracket;": "\u23b5",
+    "UnderParenthesis;": "\u23dd",
+    "Union;": "\u22c3",
+    "UnionPlus;": "\u228e",
+    "Uogon;": "\u0172",
+    "uogon;": "\u0173",
+    "Uopf;": "\U0001d54c",
+    "uopf;": "\U0001d566",
+    "UpArrow;": "\u2191",
+    "Uparrow;": "\u21d1",
+    "uparrow;": "\u2191",
+    "UpArrowBar;": "\u2912",
+    "UpArrowDownArrow;": "\u21c5",
+    "UpDownArrow;": "\u2195",
+    "Updownarrow;": "\u21d5",
+    "updownarrow;": "\u2195",
+    "UpEquilibrium;": "\u296e",
+    "upharpoonleft;": "\u21bf",
+    "upharpoonright;": "\u21be",
+    "uplus;": "\u228e",
+    "UpperLeftArrow;": "\u2196",
+    "UpperRightArrow;": "\u2197",
+    "Upsi;": "\u03d2",
+    "upsi;": "\u03c5",
+    "upsih;": "\u03d2",
+    "Upsilon;": "\u03a5",
+    "upsilon;": "\u03c5",
+    "UpTee;": "\u22a5",
+    "UpTeeArrow;": "\u21a5",
+    "upuparrows;": "\u21c8",
+    "urcorn;": "\u231d",
+    "urcorner;": "\u231d",
+    "urcrop;": "\u230e",
+    "Uring;": "\u016e",
+    "uring;": "\u016f",
+    "urtri;": "\u25f9",
+    "Uscr;": "\U0001d4b0",
+    "uscr;": "\U0001d4ca",
+    "utdot;": "\u22f0",
+    "Utilde;": "\u0168",
+    "utilde;": "\u0169",
+    "utri;": "\u25b5",
+    "utrif;": "\u25b4",
+    "uuarr;": "\u21c8",
+    "Uuml": "\xdc",
+    "uuml": "\xfc",
+    "Uuml;": "\xdc",
+    "uuml;": "\xfc",
+    "uwangle;": "\u29a7",
+    "vangrt;": "\u299c",
+    "varepsilon;": "\u03f5",
+    "varkappa;": "\u03f0",
+    "varnothing;": "\u2205",
+    "varphi;": "\u03d5",
+    "varpi;": "\u03d6",
+    "varpropto;": "\u221d",
+    "vArr;": "\u21d5",
+    "varr;": "\u2195",
+    "varrho;": "\u03f1",
+    "varsigma;": "\u03c2",
+    "varsubsetneq;": "\u228a\ufe00",
+    "varsubsetneqq;": "\u2acb\ufe00",
+    "varsupsetneq;": "\u228b\ufe00",
+    "varsupsetneqq;": "\u2acc\ufe00",
+    "vartheta;": "\u03d1",
+    "vartriangleleft;": "\u22b2",
+    "vartriangleright;": "\u22b3",
+    "Vbar;": "\u2aeb",
+    "vBar;": "\u2ae8",
+    "vBarv;": "\u2ae9",
+    "Vcy;": "\u0412",
+    "vcy;": "\u0432",
+    "VDash;": "\u22ab",
+    "Vdash;": "\u22a9",
+    "vDash;": "\u22a8",
+    "vdash;": "\u22a2",
+    "Vdashl;": "\u2ae6",
+    "Vee;": "\u22c1",
+    "vee;": "\u2228",
+    "veebar;": "\u22bb",
+    "veeeq;": "\u225a",
+    "vellip;": "\u22ee",
+    "Verbar;": "\u2016",
+    "verbar;": "|",
+    "Vert;": "\u2016",
+    "vert;": "|",
+    "VerticalBar;": "\u2223",
+    "VerticalLine;": "|",
+    "VerticalSeparator;": "\u2758",
+    "VerticalTilde;": "\u2240",
+    "VeryThinSpace;": "\u200a",
+    "Vfr;": "\U0001d519",
+    "vfr;": "\U0001d533",
+    "vltri;": "\u22b2",
+    "vnsub;": "\u2282\u20d2",
+    "vnsup;": "\u2283\u20d2",
+    "Vopf;": "\U0001d54d",
+    "vopf;": "\U0001d567",
+    "vprop;": "\u221d",
+    "vrtri;": "\u22b3",
+    "Vscr;": "\U0001d4b1",
+    "vscr;": "\U0001d4cb",
+    "vsubnE;": "\u2acb\ufe00",
+    "vsubne;": "\u228a\ufe00",
+    "vsupnE;": "\u2acc\ufe00",
+    "vsupne;": "\u228b\ufe00",
+    "Vvdash;": "\u22aa",
+    "vzigzag;": "\u299a",
+    "Wcirc;": "\u0174",
+    "wcirc;": "\u0175",
+    "wedbar;": "\u2a5f",
+    "Wedge;": "\u22c0",
+    "wedge;": "\u2227",
+    "wedgeq;": "\u2259",
+    "weierp;": "\u2118",
+    "Wfr;": "\U0001d51a",
+    "wfr;": "\U0001d534",
+    "Wopf;": "\U0001d54e",
+    "wopf;": "\U0001d568",
+    "wp;": "\u2118",
+    "wr;": "\u2240",
+    "wreath;": "\u2240",
+    "Wscr;": "\U0001d4b2",
+    "wscr;": "\U0001d4cc",
+    "xcap;": "\u22c2",
+    "xcirc;": "\u25ef",
+    "xcup;": "\u22c3",
+    "xdtri;": "\u25bd",
+    "Xfr;": "\U0001d51b",
+    "xfr;": "\U0001d535",
+    "xhArr;": "\u27fa",
+    "xharr;": "\u27f7",
+    "Xi;": "\u039e",
+    "xi;": "\u03be",
+    "xlArr;": "\u27f8",
+    "xlarr;": "\u27f5",
+    "xmap;": "\u27fc",
+    "xnis;": "\u22fb",
+    "xodot;": "\u2a00",
+    "Xopf;": "\U0001d54f",
+    "xopf;": "\U0001d569",
+    "xoplus;": "\u2a01",
+    "xotime;": "\u2a02",
+    "xrArr;": "\u27f9",
+    "xrarr;": "\u27f6",
+    "Xscr;": "\U0001d4b3",
+    "xscr;": "\U0001d4cd",
+    "xsqcup;": "\u2a06",
+    "xuplus;": "\u2a04",
+    "xutri;": "\u25b3",
+    "xvee;": "\u22c1",
+    "xwedge;": "\u22c0",
+    "Yacute": "\xdd",
+    "yacute": "\xfd",
+    "Yacute;": "\xdd",
+    "yacute;": "\xfd",
+    "YAcy;": "\u042f",
+    "yacy;": "\u044f",
+    "Ycirc;": "\u0176",
+    "ycirc;": "\u0177",
+    "Ycy;": "\u042b",
+    "ycy;": "\u044b",
+    "yen": "\xa5",
+    "yen;": "\xa5",
+    "Yfr;": "\U0001d51c",
+    "yfr;": "\U0001d536",
+    "YIcy;": "\u0407",
+    "yicy;": "\u0457",
+    "Yopf;": "\U0001d550",
+    "yopf;": "\U0001d56a",
+    "Yscr;": "\U0001d4b4",
+    "yscr;": "\U0001d4ce",
+    "YUcy;": "\u042e",
+    "yucy;": "\u044e",
+    "yuml": "\xff",
+    "Yuml;": "\u0178",
+    "yuml;": "\xff",
+    "Zacute;": "\u0179",
+    "zacute;": "\u017a",
+    "Zcaron;": "\u017d",
+    "zcaron;": "\u017e",
+    "Zcy;": "\u0417",
+    "zcy;": "\u0437",
+    "Zdot;": "\u017b",
+    "zdot;": "\u017c",
+    "zeetrf;": "\u2128",
+    "ZeroWidthSpace;": "\u200b",
+    "Zeta;": "\u0396",
+    "zeta;": "\u03b6",
+    "Zfr;": "\u2128",
+    "zfr;": "\U0001d537",
+    "ZHcy;": "\u0416",
+    "zhcy;": "\u0436",
+    "zigrarr;": "\u21dd",
+    "Zopf;": "\u2124",
+    "zopf;": "\U0001d56b",
+    "Zscr;": "\U0001d4b5",
+    "zscr;": "\U0001d4cf",
+    "zwj;": "\u200d",
+    "zwnj;": "\u200c",
+}
+
+# maps the Unicode codepoint to the HTML entity name
+codepoint2name = {}
+
+# maps the HTML entity name to the character
+# (or a character reference if the character is outside the Latin-1 range)
+entitydefs = {}
+
+for name, codepoint in name2codepoint.items():
+    codepoint2name[codepoint] = name
+    entitydefs[name] = chr(codepoint)
+
+del name, codepoint
diff --git a/unix-ffi/html.entities/manifest.py b/unix-ffi/html.entities/manifest.py
new file mode 100644
index 000000000..a985d2821
--- /dev/null
+++ b/unix-ffi/html.entities/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="3.3.4")
+
+package("html")
diff --git a/unix-ffi/html.parser/html/parser.py b/unix-ffi/html.parser/html/parser.py
new file mode 100644
index 000000000..74b39d49a
--- /dev/null
+++ b/unix-ffi/html.parser/html/parser.py
@@ -0,0 +1,537 @@
+"""A parser for HTML and XHTML."""
+
+# This file is based on sgmllib.py, but the API is slightly different.
+
+# XXX There should be a way to distinguish between PCDATA (parsed
+# character data -- the normal case), RCDATA (replaceable character
+# data -- only char and entity references and end tags are special)
+# and CDATA (character data -- only end tags are special).
+
+
+import _markupbase
+import re
+import warnings
+
+# Regular expressions used for parsing
+
+interesting_normal = re.compile("[&<]")
+incomplete = re.compile("&[a-zA-Z#]")
+
+entityref = re.compile("&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]")
+charref = re.compile("&#(?:[0-9]+|[xX][0-9a-fA-F]+)[^0-9a-fA-F]")
+
+starttagopen = re.compile("<[a-zA-Z]")
+piclose = re.compile(">")
+commentclose = re.compile(r"--\s*>")
+tagfind = re.compile("([a-zA-Z][-.a-zA-Z0-9:_]*)(?:\s|/(?!>))*")
+# see http://www.w3.org/TR/html5/tokenization.html#tag-open-state
+# and http://www.w3.org/TR/html5/tokenization.html#tag-name-state
+tagfind_tolerant = re.compile("[a-zA-Z][^\t\n\r\f />\\x00]*")
+# Note:
+#  1) the strict attrfind isn't really strict, but we can't make it
+#     correctly strict without breaking backward compatibility;
+#  2) if you change attrfind remember to update locatestarttagend too;
+#  3) if you change attrfind and/or locatestarttagend the parser will
+#     explode, so don't do it.
+attrfind = re.compile(
+    r"\s*([a-zA-Z_][-.:a-zA-Z_0-9]*)(\s*=\s*" r'(\'[^\']*\'|"[^"]*"|[^\s"\'=<>`]*))?'
+)
+attrfind_tolerant = re.compile(
+    r'((?<=[\'"\s/])[^\s/>][^\s/=>]*)(\s*=+\s*'
+    r'(\'[^\']*\'|"[^"]*"|(?![\'"])[^>\s]*))?(?:\s|/(?!>))*'
+)
+locatestarttagend = re.compile(
+    r"""
+  <[a-zA-Z][-.a-zA-Z0-9:_]*          # tag name
+  (?:\s+                             # whitespace before attribute name
+    (?:[a-zA-Z_][-.:a-zA-Z0-9_]*     # attribute name
+      (?:\s*=\s*                     # value indicator
+        (?:'[^']*'                   # LITA-enclosed value
+          |\"[^\"]*\"                # LIT-enclosed value
+          |[^'\">\s]+                # bare value
+         )
+       )?
+     )
+   )*
+  \s*                                # trailing whitespace
+""",
+    re.VERBOSE,
+)
+locatestarttagend_tolerant = re.compile(
+    r"""
+  <[a-zA-Z][-.a-zA-Z0-9:_]*          # tag name
+  (?:[\s/]*                          # optional whitespace before attribute name
+    (?:(?<=['"\s/])[^\s/>][^\s/=>]*  # attribute name
+      (?:\s*=+\s*                    # value indicator
+        (?:'[^']*'                   # LITA-enclosed value
+          |"[^"]*"                   # LIT-enclosed value
+          |(?!['"])[^>\s]*           # bare value
+         )
+         (?:\s*,)*                   # possibly followed by a comma
+       )?(?:\s|/(?!>))*
+     )*
+   )?
+  \s*                                # trailing whitespace
+""",
+    re.VERBOSE,
+)
+endendtag = re.compile(">")
+# the HTML 5 spec, section 8.1.2.2, doesn't allow spaces between
+# </ and the tag name, so maybe this should be fixed
+endtagfind = re.compile("</\s*([a-zA-Z][-.a-zA-Z0-9:_]*)\s*>")
+
+
+class HTMLParseError(Exception):
+    """Exception raised for all parse errors."""
+
+    def __init__(self, msg, position=(None, None)):
+        assert msg
+        self.msg = msg
+        self.lineno = position[0]
+        self.offset = position[1]
+
+    def __str__(self):
+        result = self.msg
+        if self.lineno is not None:
+            result = result + ", at line %d" % self.lineno
+        if self.offset is not None:
+            result = result + ", column %d" % (self.offset + 1)
+        return result
+
+
+class HTMLParser(_markupbase.ParserBase):
+    """Find tags and other markup and call handler functions.
+
+    Usage:
+        p = HTMLParser()
+        p.feed(data)
+        ...
+        p.close()
+
+    Start tags are handled by calling self.handle_starttag() or
+    self.handle_startendtag(); end tags by self.handle_endtag().  The
+    data between tags is passed from the parser to the derived class
+    by calling self.handle_data() with the data as argument (the data
+    may be split up in arbitrary chunks).  Entity references are
+    passed by calling self.handle_entityref() with the entity
+    reference as the argument.  Numeric character references are
+    passed to self.handle_charref() with the string containing the
+    reference as the argument.
+    """
+
+    CDATA_CONTENT_ELEMENTS = ("script", "style")
+
+    def __init__(self, strict=False):
+        """Initialize and reset this instance.
+
+        If strict is set to False (the default) the parser will parse invalid
+        markup, otherwise it will raise an error.  Note that the strict mode
+        is deprecated.
+        """
+        if strict:
+            warnings.warn("The strict mode is deprecated.", DeprecationWarning, stacklevel=2)
+        self.strict = strict
+        self.reset()
+
+    def reset(self):
+        """Reset this instance.  Loses all unprocessed data."""
+        self.rawdata = ""
+        self.lasttag = "???"
+        self.interesting = interesting_normal
+        self.cdata_elem = None
+        _markupbase.ParserBase.reset(self)
+
+    def feed(self, data):
+        r"""Feed data to the parser.
+
+        Call this as often as you want, with as little or as much text
+        as you want (may include '\n').
+        """
+        self.rawdata = self.rawdata + data
+        self.goahead(0)
+
+    def close(self):
+        """Handle any buffered data."""
+        self.goahead(1)
+
+    def error(self, message):
+        raise HTMLParseError(message, self.getpos())
+
+    __starttag_text = None
+
+    def get_starttag_text(self):
+        """Return full source of start tag: '<...>'."""
+        return self.__starttag_text
+
+    def set_cdata_mode(self, elem):
+        self.cdata_elem = elem.lower()
+        self.interesting = re.compile(r"</\s*%s\s*>" % self.cdata_elem, re.I)
+
+    def clear_cdata_mode(self):
+        self.interesting = interesting_normal
+        self.cdata_elem = None
+
+    # Internal -- handle data as far as reasonable.  May leave state
+    # and data to be processed by a subsequent call.  If 'end' is
+    # true, force handling all data as if followed by EOF marker.
+    def goahead(self, end):
+        rawdata = self.rawdata
+        i = 0
+        n = len(rawdata)
+        while i < n:
+            match = self.interesting.search(rawdata, i)  # < or &
+            if match:
+                j = match.start()
+            else:
+                if self.cdata_elem:
+                    break
+                j = n
+            if i < j:
+                self.handle_data(rawdata[i:j])
+            i = self.updatepos(i, j)
+            if i == n:
+                break
+            startswith = rawdata.startswith
+            if startswith("<", i):
+                if starttagopen.match(rawdata, i):  # < + letter
+                    k = self.parse_starttag(i)
+                elif startswith("</", i):
+                    k = self.parse_endtag(i)
+                elif startswith("<!--", i):
+                    k = self.parse_comment(i)
+                elif startswith("<?", i):
+                    k = self.parse_pi(i)
+                elif startswith("<!", i):
+                    if self.strict:
+                        k = self.parse_declaration(i)
+                    else:
+                        k = self.parse_html_declaration(i)
+                elif (i + 1) < n:
+                    self.handle_data("<")
+                    k = i + 1
+                else:
+                    break
+                if k < 0:
+                    if not end:
+                        break
+                    if self.strict:
+                        self.error("EOF in middle of construct")
+                    k = rawdata.find(">", i + 1)
+                    if k < 0:
+                        k = rawdata.find("<", i + 1)
+                        if k < 0:
+                            k = i + 1
+                    else:
+                        k += 1
+                    self.handle_data(rawdata[i:k])
+                i = self.updatepos(i, k)
+            elif startswith("&#", i):
+                match = charref.match(rawdata, i)
+                if match:
+                    name = match.group()[2:-1]
+                    self.handle_charref(name)
+                    k = match.end()
+                    if not startswith(";", k - 1):
+                        k = k - 1
+                    i = self.updatepos(i, k)
+                    continue
+                else:
+                    if ";" in rawdata[i:]:  # bail by consuming &#
+                        self.handle_data(rawdata[0:2])
+                        i = self.updatepos(i, 2)
+                    break
+            elif startswith("&", i):
+                match = entityref.match(rawdata, i)
+                if match:
+                    name = match.group(1)
+                    self.handle_entityref(name)
+                    k = match.end()
+                    if not startswith(";", k - 1):
+                        k = k - 1
+                    i = self.updatepos(i, k)
+                    continue
+                match = incomplete.match(rawdata, i)
+                if match:
+                    # match.group() will contain at least 2 chars
+                    if end and match.group() == rawdata[i:]:
+                        if self.strict:
+                            self.error("EOF in middle of entity or char ref")
+                        else:
+                            k = match.end()
+                            if k <= i:
+                                k = n
+                            i = self.updatepos(i, i + 1)
+                    # incomplete
+                    break
+                elif (i + 1) < n:
+                    # not the end of the buffer, and can't be confused
+                    # with some other construct
+                    self.handle_data("&")
+                    i = self.updatepos(i, i + 1)
+                else:
+                    break
+            else:
+                assert 0, "interesting.search() lied"
+        # end while
+        if end and i < n and not self.cdata_elem:
+            self.handle_data(rawdata[i:n])
+            i = self.updatepos(i, n)
+        self.rawdata = rawdata[i:]
+
+    # Internal -- parse html declarations, return length or -1 if not terminated
+    # See w3.org/TR/html5/tokenization.html#markup-declaration-open-state
+    # See also parse_declaration in _markupbase
+    def parse_html_declaration(self, i):
+        rawdata = self.rawdata
+        assert rawdata[i : i + 2] == "<!", "unexpected call to " "parse_html_declaration()"
+        if rawdata[i : i + 4] == "<!--":
+            # this case is actually already handled in goahead()
+            return self.parse_comment(i)
+        elif rawdata[i : i + 3] == "<![":
+            return self.parse_marked_section(i)
+        elif rawdata[i : i + 9].lower() == "<!doctype":
+            # find the closing >
+            gtpos = rawdata.find(">", i + 9)
+            if gtpos == -1:
+                return -1
+            self.handle_decl(rawdata[i + 2 : gtpos])
+            return gtpos + 1
+        else:
+            return self.parse_bogus_comment(i)
+
+    # Internal -- parse bogus comment, return length or -1 if not terminated
+    # see http://www.w3.org/TR/html5/tokenization.html#bogus-comment-state
+    def parse_bogus_comment(self, i, report=1):
+        rawdata = self.rawdata
+        assert rawdata[i : i + 2] in ("<!", "</"), "unexpected call to " "parse_comment()"
+        pos = rawdata.find(">", i + 2)
+        if pos == -1:
+            return -1
+        if report:
+            self.handle_comment(rawdata[i + 2 : pos])
+        return pos + 1
+
+    # Internal -- parse processing instr, return end or -1 if not terminated
+    def parse_pi(self, i):
+        rawdata = self.rawdata
+        assert rawdata[i : i + 2] == "<?", "unexpected call to parse_pi()"
+        match = piclose.search(rawdata, i + 2)  # >
+        if not match:
+            return -1
+        j = match.start()
+        self.handle_pi(rawdata[i + 2 : j])
+        j = match.end()
+        return j
+
+    # Internal -- handle starttag, return end or -1 if not terminated
+    def parse_starttag(self, i):
+        self.__starttag_text = None
+        endpos = self.check_for_whole_start_tag(i)
+        if endpos < 0:
+            return endpos
+        rawdata = self.rawdata
+        self.__starttag_text = rawdata[i:endpos]
+
+        # Now parse the data between i+1 and j into a tag and attrs
+        attrs = []
+        match = tagfind.match(rawdata, i + 1)
+        assert match, "unexpected call to parse_starttag()"
+        k = match.end()
+        self.lasttag = tag = match.group(1).lower()
+        while k < endpos:
+            if self.strict:
+                m = attrfind.match(rawdata, k)
+            else:
+                m = attrfind_tolerant.match(rawdata, k)
+            if not m:
+                break
+            attrname, rest, attrvalue = m.group(1, 2, 3)
+            if not rest:
+                attrvalue = None
+            elif attrvalue[:1] == "'" == attrvalue[-1:] or attrvalue[:1] == '"' == attrvalue[-1:]:
+                attrvalue = attrvalue[1:-1]
+            if attrvalue:
+                attrvalue = self.unescape(attrvalue)
+            attrs.append((attrname.lower(), attrvalue))
+            k = m.end()
+
+        end = rawdata[k:endpos].strip()
+        if end not in (">", "/>"):
+            lineno, offset = self.getpos()
+            if "\n" in self.__starttag_text:
+                lineno = lineno + self.__starttag_text.count("\n")
+                offset = len(self.__starttag_text) - self.__starttag_text.rfind("\n")
+            else:
+                offset = offset + len(self.__starttag_text)
+            if self.strict:
+                self.error("junk characters in start tag: %r" % (rawdata[k:endpos][:20],))
+            self.handle_data(rawdata[i:endpos])
+            return endpos
+        if end.endswith("/>"):
+            # XHTML-style empty tag: <span attr="value" />
+            self.handle_startendtag(tag, attrs)
+        else:
+            self.handle_starttag(tag, attrs)
+            if tag in self.CDATA_CONTENT_ELEMENTS:
+                self.set_cdata_mode(tag)
+        return endpos
+
+    # Internal -- check to see if we have a complete starttag; return end
+    # or -1 if incomplete.
+    def check_for_whole_start_tag(self, i):
+        rawdata = self.rawdata
+        if self.strict:
+            m = locatestarttagend.match(rawdata, i)
+        else:
+            m = locatestarttagend_tolerant.match(rawdata, i)
+        if m:
+            j = m.end()
+            next = rawdata[j : j + 1]
+            if next == ">":
+                return j + 1
+            if next == "/":
+                if rawdata.startswith("/>", j):
+                    return j + 2
+                if rawdata.startswith("/", j):
+                    # buffer boundary
+                    return -1
+                # else bogus input
+                if self.strict:
+                    self.updatepos(i, j + 1)
+                    self.error("malformed empty start tag")
+                if j > i:
+                    return j
+                else:
+                    return i + 1
+            if next == "":
+                # end of input
+                return -1
+            if next in ("abcdefghijklmnopqrstuvwxyz=/" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
+                # end of input in or before attribute value, or we have the
+                # '/' from a '/>' ending
+                return -1
+            if self.strict:
+                self.updatepos(i, j)
+                self.error("malformed start tag")
+            if j > i:
+                return j
+            else:
+                return i + 1
+        raise AssertionError("we should not get here!")
+
+    # Internal -- parse endtag, return end or -1 if incomplete
+    def parse_endtag(self, i):
+        rawdata = self.rawdata
+        assert rawdata[i : i + 2] == "</", "unexpected call to parse_endtag"
+        match = endendtag.search(rawdata, i + 1)  # >
+        if not match:
+            return -1
+        gtpos = match.end()
+        match = endtagfind.match(rawdata, i)  # </ + tag + >
+        if not match:
+            if self.cdata_elem is not None:
+                self.handle_data(rawdata[i:gtpos])
+                return gtpos
+            if self.strict:
+                self.error("bad end tag: %r" % (rawdata[i:gtpos],))
+            # find the name: w3.org/TR/html5/tokenization.html#tag-name-state
+            namematch = tagfind_tolerant.match(rawdata, i + 2)
+            if not namematch:
+                # w3.org/TR/html5/tokenization.html#end-tag-open-state
+                if rawdata[i : i + 3] == "</>":
+                    return i + 3
+                else:
+                    return self.parse_bogus_comment(i)
+            tagname = namematch.group().lower()
+            # consume and ignore other stuff between the name and the >
+            # Note: this is not 100% correct, since we might have things like
+            # </tag attr=">">, but looking for > after tha name should cover
+            # most of the cases and is much simpler
+            gtpos = rawdata.find(">", namematch.end())
+            self.handle_endtag(tagname)
+            return gtpos + 1
+
+        elem = match.group(1).lower()  # script or style
+        if self.cdata_elem is not None:
+            if elem != self.cdata_elem:
+                self.handle_data(rawdata[i:gtpos])
+                return gtpos
+
+        self.handle_endtag(elem.lower())
+        self.clear_cdata_mode()
+        return gtpos
+
+    # Overridable -- finish processing of start+end tag: <tag.../>
+    def handle_startendtag(self, tag, attrs):
+        self.handle_starttag(tag, attrs)
+        self.handle_endtag(tag)
+
+    # Overridable -- handle start tag
+    def handle_starttag(self, tag, attrs):
+        pass
+
+    # Overridable -- handle end tag
+    def handle_endtag(self, tag):
+        pass
+
+    # Overridable -- handle character reference
+    def handle_charref(self, name):
+        pass
+
+    # Overridable -- handle entity reference
+    def handle_entityref(self, name):
+        pass
+
+    # Overridable -- handle data
+    def handle_data(self, data):
+        pass
+
+    # Overridable -- handle comment
+    def handle_comment(self, data):
+        pass
+
+    # Overridable -- handle declaration
+    def handle_decl(self, decl):
+        pass
+
+    # Overridable -- handle processing instruction
+    def handle_pi(self, data):
+        pass
+
+    def unknown_decl(self, data):
+        if self.strict:
+            self.error("unknown declaration: %r" % (data,))
+
+    # Internal -- helper to remove special character quoting
+    def unescape(self, s):
+        if "&" not in s:
+            return s
+
+        def replaceEntities(s):
+            s = s.groups()[0]
+            try:
+                if s[0] == "#":
+                    s = s[1:]
+                    if s[0] in ["x", "X"]:
+                        c = int(s[1:].rstrip(";"), 16)
+                    else:
+                        c = int(s.rstrip(";"))
+                    return chr(c)
+            except ValueError:
+                return "&#" + s
+            else:
+                from html.entities import html5
+
+                if s in html5:
+                    return html5[s]
+                elif s.endswith(";"):
+                    return "&" + s
+                for x in range(2, len(s)):
+                    if s[:x] in html5:
+                        return html5[s[:x]] + s[x:]
+                else:
+                    return "&" + s
+
+        return re.sub(
+            r"&(#?[xX]?(?:[0-9a-fA-F]+;|\w{1,32};?))", replaceEntities, s, flags=re.ASCII
+        )
diff --git a/unix-ffi/html.parser/manifest.py b/unix-ffi/html.parser/manifest.py
new file mode 100644
index 000000000..3f29bbceb
--- /dev/null
+++ b/unix-ffi/html.parser/manifest.py
@@ -0,0 +1,8 @@
+metadata(version="3.3.4")
+
+require("_markupbase")
+require("warnings")
+require("html.entities")
+require("re")
+
+package("html")
diff --git a/unix-ffi/http.client/example_client.py b/unix-ffi/http.client/example_client.py
new file mode 100644
index 000000000..d55677306
--- /dev/null
+++ b/unix-ffi/http.client/example_client.py
@@ -0,0 +1,9 @@
+from http.client import HTTPConnection
+
+
+conn = HTTPConnection("localhost")
+# conn = HTTPConnection("python.org")
+conn.request("GET", "/")
+resp = conn.getresponse()
+print(resp)
+print(resp.read())
diff --git a/unix-ffi/http.client/http/client.py b/unix-ffi/http.client/http/client.py
new file mode 100644
index 000000000..856848283
--- /dev/null
+++ b/unix-ffi/http.client/http/client.py
@@ -0,0 +1,1332 @@
+"""HTTP/1.1 client library
+
+<intro stuff goes here>
+<other stuff, too>
+
+HTTPConnection goes through a number of "states", which define when a client
+may legally make another request or fetch the response for a particular
+request. This diagram details these state transitions:
+
+    (null)
+      |
+      | HTTPConnection()
+      v
+    Idle
+      |
+      | putrequest()
+      v
+    Request-started
+      |
+      | ( putheader() )*  endheaders()
+      v
+    Request-sent
+      |
+      | response = getresponse()
+      v
+    Unread-response   [Response-headers-read]
+      |\____________________
+      |                     |
+      | response.read()     | putrequest()
+      v                     v
+    Idle                  Req-started-unread-response
+                     ______/|
+                   /        |
+   response.read() |        | ( putheader() )*  endheaders()
+                   v        v
+       Request-started    Req-sent-unread-response
+                            |
+                            | response.read()
+                            v
+                          Request-sent
+
+This diagram presents the following rules:
+  -- a second request may not be started until {response-headers-read}
+  -- a response [object] cannot be retrieved until {request-sent}
+  -- there is no differentiation between an unread response body and a
+     partially read response body
+
+Note: this enforcement is applied by the HTTPConnection class. The
+      HTTPResponse class does not enforce this state machine, which
+      implies sophisticated clients may accelerate the request/response
+      pipeline. Caution should be taken, though: accelerating the states
+      beyond the above pattern may imply knowledge of the server's
+      connection-close behavior for certain requests. For example, it
+      is impossible to tell whether the server will close the connection
+      UNTIL the response headers have been read; this means that further
+      requests cannot be placed into the pipeline until it is known that
+      the server will NOT be closing the connection.
+
+Logical State                  __state            __response
+-------------                  -------            ----------
+Idle                           _CS_IDLE           None
+Request-started                _CS_REQ_STARTED    None
+Request-sent                   _CS_REQ_SENT       None
+Unread-response                _CS_IDLE           <response_class>
+Req-started-unread-response    _CS_REQ_STARTED    <response_class>
+Req-sent-unread-response       _CS_REQ_SENT       <response_class>
+"""
+
+import email.parser
+import email.message
+import io
+import os
+import socket
+import collections
+from urllib.parse import urlsplit
+import warnings
+
+__all__ = [
+    "HTTPResponse",
+    "HTTPConnection",
+    "HTTPException",
+    "NotConnected",
+    "UnknownProtocol",
+    "UnknownTransferEncoding",
+    "UnimplementedFileMode",
+    "IncompleteRead",
+    "InvalidURL",
+    "ImproperConnectionState",
+    "CannotSendRequest",
+    "CannotSendHeader",
+    "ResponseNotReady",
+    "BadStatusLine",
+    "error",
+    "responses",
+]
+
+HTTP_PORT = 80
+HTTPS_PORT = 443
+
+_UNKNOWN = "UNKNOWN"
+
+# connection states
+_CS_IDLE = "Idle"
+_CS_REQ_STARTED = "Request-started"
+_CS_REQ_SENT = "Request-sent"
+
+# status codes
+# informational
+CONTINUE = 100
+SWITCHING_PROTOCOLS = 101
+PROCESSING = 102
+
+# successful
+OK = 200
+CREATED = 201
+ACCEPTED = 202
+NON_AUTHORITATIVE_INFORMATION = 203
+NO_CONTENT = 204
+RESET_CONTENT = 205
+PARTIAL_CONTENT = 206
+MULTI_STATUS = 207
+IM_USED = 226
+
+# redirection
+MULTIPLE_CHOICES = 300
+MOVED_PERMANENTLY = 301
+FOUND = 302
+SEE_OTHER = 303
+NOT_MODIFIED = 304
+USE_PROXY = 305
+TEMPORARY_REDIRECT = 307
+
+# client error
+BAD_REQUEST = 400
+UNAUTHORIZED = 401
+PAYMENT_REQUIRED = 402
+FORBIDDEN = 403
+NOT_FOUND = 404
+METHOD_NOT_ALLOWED = 405
+NOT_ACCEPTABLE = 406
+PROXY_AUTHENTICATION_REQUIRED = 407
+REQUEST_TIMEOUT = 408
+CONFLICT = 409
+GONE = 410
+LENGTH_REQUIRED = 411
+PRECONDITION_FAILED = 412
+REQUEST_ENTITY_TOO_LARGE = 413
+REQUEST_URI_TOO_LONG = 414
+UNSUPPORTED_MEDIA_TYPE = 415
+REQUESTED_RANGE_NOT_SATISFIABLE = 416
+EXPECTATION_FAILED = 417
+UNPROCESSABLE_ENTITY = 422
+LOCKED = 423
+FAILED_DEPENDENCY = 424
+UPGRADE_REQUIRED = 426
+PRECONDITION_REQUIRED = 428
+TOO_MANY_REQUESTS = 429
+REQUEST_HEADER_FIELDS_TOO_LARGE = 431
+
+# server error
+INTERNAL_SERVER_ERROR = 500
+NOT_IMPLEMENTED = 501
+BAD_GATEWAY = 502
+SERVICE_UNAVAILABLE = 503
+GATEWAY_TIMEOUT = 504
+HTTP_VERSION_NOT_SUPPORTED = 505
+INSUFFICIENT_STORAGE = 507
+NOT_EXTENDED = 510
+NETWORK_AUTHENTICATION_REQUIRED = 511
+
+# Mapping status codes to official W3C names
+responses = {
+    100: "Continue",
+    101: "Switching Protocols",
+    200: "OK",
+    201: "Created",
+    202: "Accepted",
+    203: "Non-Authoritative Information",
+    204: "No Content",
+    205: "Reset Content",
+    206: "Partial Content",
+    300: "Multiple Choices",
+    301: "Moved Permanently",
+    302: "Found",
+    303: "See Other",
+    304: "Not Modified",
+    305: "Use Proxy",
+    306: "(Unused)",
+    307: "Temporary Redirect",
+    400: "Bad Request",
+    401: "Unauthorized",
+    402: "Payment Required",
+    403: "Forbidden",
+    404: "Not Found",
+    405: "Method Not Allowed",
+    406: "Not Acceptable",
+    407: "Proxy Authentication Required",
+    408: "Request Timeout",
+    409: "Conflict",
+    410: "Gone",
+    411: "Length Required",
+    412: "Precondition Failed",
+    413: "Request Entity Too Large",
+    414: "Request-URI Too Long",
+    415: "Unsupported Media Type",
+    416: "Requested Range Not Satisfiable",
+    417: "Expectation Failed",
+    428: "Precondition Required",
+    429: "Too Many Requests",
+    431: "Request Header Fields Too Large",
+    500: "Internal Server Error",
+    501: "Not Implemented",
+    502: "Bad Gateway",
+    503: "Service Unavailable",
+    504: "Gateway Timeout",
+    505: "HTTP Version Not Supported",
+    511: "Network Authentication Required",
+}
+
+# maximal amount of data to read at one time in _safe_read
+MAXAMOUNT = 1048576
+
+# maximal line length when calling readline().
+_MAXLINE = 65536
+_MAXHEADERS = 100
+
+
+class HTTPMessage(email.message.Message):
+    # XXX The only usage of this method is in
+    # http.server.CGIHTTPRequestHandler.  Maybe move the code there so
+    # that it doesn't need to be part of the public API.  The API has
+    # never been defined so this could cause backwards compatibility
+    # issues.
+
+    def getallmatchingheaders(self, name):
+        """Find all header lines matching a given header name.
+
+        Look through the list of headers and find all lines matching a given
+        header name (and their continuation lines).  A list of the lines is
+        returned, without interpretation.  If the header does not occur, an
+        empty list is returned.  If the header occurs multiple times, all
+        occurrences are returned.  Case is not important in the header name.
+
+        """
+        name = name.lower() + ":"
+        n = len(name)
+        lst = []
+        hit = 0
+        for line in self.keys():
+            if line[:n].lower() == name:
+                hit = 1
+            elif not line[:1].isspace():
+                hit = 0
+            if hit:
+                lst.append(line)
+        return lst
+
+
+def parse_headers(fp, _class=HTTPMessage):
+    """Parses only RFC2822 headers from a file pointer.
+
+    email Parser wants to see strings rather than bytes.
+    But a TextIOWrapper around self.rfile would buffer too many bytes
+    from the stream, bytes which we later need to read as bytes.
+    So we read the correct bytes here, as bytes, for email Parser
+    to parse.
+
+    """
+    headers = []
+    while True:
+        line = fp.readline(_MAXLINE + 1)
+        if len(line) > _MAXLINE:
+            raise LineTooLong("header line")
+        headers.append(line)
+        if len(headers) > _MAXHEADERS:
+            raise HTTPException("got more than %d headers" % _MAXHEADERS)
+        if line in (b"\r\n", b"\n", b""):
+            break
+    hstring = b"".join(headers).decode("iso-8859-1")
+    return email.parser.Parser(_class=_class).parsestr(hstring)
+
+
+_strict_sentinel = object()
+
+
+class HTTPResponse:
+    # class HTTPResponse(io.RawIOBase):
+
+    # See RFC 2616 sec 19.6 and RFC 1945 sec 6 for details.
+
+    # The bytes from the socket object are iso-8859-1 strings.
+    # See RFC 2616 sec 2.2 which notes an exception for MIME-encoded
+    # text following RFC 2047.  The basic status line parsing only
+    # accepts iso-8859-1.
+
+    def __init__(self, sock, debuglevel=0, strict=_strict_sentinel, method=None, url=None):
+        # If the response includes a content-length header, we need to
+        # make sure that the client doesn't read more than the
+        # specified number of bytes.  If it does, it will block until
+        # the server times out and closes the connection.  This will
+        # happen if a self.fp.read() is done (without a size) whether
+        # self.fp is buffered or not.  So, no self.fp.read() by
+        # clients unless they know what they are doing.
+        self.fp = sock.makefile("rb")
+        self.debuglevel = debuglevel
+        if strict is not _strict_sentinel:
+            warnings.warn(
+                "the 'strict' argument isn't supported anymore; "
+                "http.client now always assumes HTTP/1.x compliant servers.",
+                DeprecationWarning,
+                2,
+            )
+        self._method = method
+
+        # The HTTPResponse object is returned via urllib.  The clients
+        # of http and urllib expect different attributes for the
+        # headers.  headers is used here and supports urllib.  msg is
+        # provided as a backwards compatibility layer for http
+        # clients.
+
+        self.headers = self.msg = None
+
+        # from the Status-Line of the response
+        self.version = _UNKNOWN  # HTTP-Version
+        self.status = _UNKNOWN  # Status-Code
+        self.reason = _UNKNOWN  # Reason-Phrase
+
+        self.chunked = _UNKNOWN  # is "chunked" being used?
+        self.chunk_left = _UNKNOWN  # bytes left to read in current chunk
+        self.length = _UNKNOWN  # number of bytes left in response
+        self.will_close = _UNKNOWN  # conn will close at end of response
+
+    def _read_status(self):
+        line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
+        if len(line) > _MAXLINE:
+            raise LineTooLong("status line")
+        if self.debuglevel > 0:
+            print("reply:", repr(line))
+        if not line:
+            # Presumably, the server closed the connection before
+            # sending a valid response.
+            raise BadStatusLine(line)
+        try:
+            version, status, reason = line.split(None, 2)
+        except ValueError:
+            try:
+                version, status = line.split(None, 1)
+                reason = ""
+            except ValueError:
+                # empty version will cause next test to fail.
+                version = ""
+        if not version.startswith("HTTP/"):
+            self._close_conn()
+            raise BadStatusLine(line)
+
+        # The status code is a three-digit number
+        try:
+            status = int(status)
+            if status < 100 or status > 999:
+                raise BadStatusLine(line)
+        except ValueError:
+            raise BadStatusLine(line)
+        return version, status, reason
+
+    def begin(self):
+        if self.headers is not None:
+            # we've already started reading the response
+            return
+
+        # read until we get a non-100 response
+        while True:
+            version, status, reason = self._read_status()
+            if status != CONTINUE:
+                break
+            # skip the header from the 100 response
+            while True:
+                skip = self.fp.readline(_MAXLINE + 1)
+                if len(skip) > _MAXLINE:
+                    raise LineTooLong("header line")
+                skip = skip.strip()
+                if not skip:
+                    break
+                if self.debuglevel > 0:
+                    print("header:", skip)
+
+        self.code = self.status = status
+        self.reason = reason.strip()
+        if version in ("HTTP/1.0", "HTTP/0.9"):
+            # Some servers might still return "0.9", treat it as 1.0 anyway
+            self.version = 10
+        elif version.startswith("HTTP/1."):
+            self.version = 11  # use HTTP/1.1 code for HTTP/1.x where x>=1
+        else:
+            raise UnknownProtocol(version)
+
+        self.headers = self.msg = parse_headers(self.fp)
+
+        if self.debuglevel > 0:
+            for hdr in self.headers:
+                print("header:", hdr, end=" ")
+
+        # are we using the chunked-style of transfer encoding?
+        tr_enc = self.headers.get("transfer-encoding")
+        if tr_enc and tr_enc.lower() == "chunked":
+            self.chunked = True
+            self.chunk_left = None
+        else:
+            self.chunked = False
+
+        # will the connection close at the end of the response?
+        self.will_close = self._check_close()
+
+        # do we have a Content-Length?
+        # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked"
+        self.length = None
+        length = self.headers.get("content-length")
+
+        # are we using the chunked-style of transfer encoding?
+        tr_enc = self.headers.get("transfer-encoding")
+        if length and not self.chunked:
+            try:
+                self.length = int(length)
+            except ValueError:
+                self.length = None
+            else:
+                if self.length < 0:  # ignore nonsensical negative lengths
+                    self.length = None
+        else:
+            self.length = None
+
+        # does the body have a fixed length? (of zero)
+        if (
+            status == NO_CONTENT
+            or status == NOT_MODIFIED
+            or 100 <= status < 200
+            or self._method == "HEAD"  # 1xx codes
+        ):
+            self.length = 0
+
+        # if the connection remains open, and we aren't using chunked, and
+        # a content-length was not provided, then assume that the connection
+        # WILL close.
+        if not self.will_close and not self.chunked and self.length is None:
+            self.will_close = True
+
+    def _check_close(self):
+        conn = self.headers.get("connection")
+        if self.version == 11:
+            # An HTTP/1.1 proxy is assumed to stay open unless
+            # explicitly closed.
+            conn = self.headers.get("connection")
+            if conn and "close" in conn.lower():
+                return True
+            return False
+
+        # Some HTTP/1.0 implementations have support for persistent
+        # connections, using rules different than HTTP/1.1.
+
+        # For older HTTP, Keep-Alive indicates persistent connection.
+        if self.headers.get("keep-alive"):
+            return False
+
+        # At least Akamai returns a "Connection: Keep-Alive" header,
+        # which was supposed to be sent by the client.
+        if conn and "keep-alive" in conn.lower():
+            return False
+
+        # Proxy-Connection is a netscape hack.
+        pconn = self.headers.get("proxy-connection")
+        if pconn and "keep-alive" in pconn.lower():
+            return False
+
+        # otherwise, assume it will close
+        return True
+
+    def _close_conn(self):
+        fp = self.fp
+        self.fp = None
+        fp.close()
+
+    def close(self):
+        super().close()  # set "closed" flag
+        if self.fp:
+            self._close_conn()
+
+    # These implementations are for the benefit of io.BufferedReader.
+
+    # XXX This class should probably be revised to act more like
+    # the "raw stream" that BufferedReader expects.
+
+    def flush(self):
+        super().flush()
+        if self.fp:
+            self.fp.flush()
+
+    def readable(self):
+        return True
+
+    # End of "raw stream" methods
+
+    def isclosed(self):
+        """True if the connection is closed."""
+        # NOTE: it is possible that we will not ever call self.close(). This
+        #       case occurs when will_close is TRUE, length is None, and we
+        #       read up to the last byte, but NOT past it.
+        #
+        # IMPLIES: if will_close is FALSE, then self.close() will ALWAYS be
+        #          called, meaning self.isclosed() is meaningful.
+        return self.fp is None
+
+    def read(self, amt=None):
+        if self.fp is None:
+            return b""
+
+        if self._method == "HEAD":
+            self._close_conn()
+            return b""
+
+        if amt is not None:
+            # Amount is given, so call base class version
+            # (which is implemented in terms of self.readinto)
+            return super(HTTPResponse, self).read(amt)
+        else:
+            # Amount is not given (unbounded read) so we must check self.length
+            # and self.chunked
+
+            if self.chunked:
+                return self._readall_chunked()
+
+            if self.length is None:
+                s = self.fp.read()
+            else:
+                try:
+                    s = self._safe_read(self.length)
+                except IncompleteRead:
+                    self._close_conn()
+                    raise
+                self.length = 0
+            self._close_conn()  # we read everything
+            return s
+
+    def readinto(self, b):
+        if self.fp is None:
+            return 0
+
+        if self._method == "HEAD":
+            self._close_conn()
+            return 0
+
+        if self.chunked:
+            return self._readinto_chunked(b)
+
+        if self.length is not None:
+            if len(b) > self.length:
+                # clip the read to the "end of response"
+                b = memoryview(b)[0 : self.length]
+
+        # we do not use _safe_read() here because this may be a .will_close
+        # connection, and the user is reading more bytes than will be provided
+        # (for example, reading in 1k chunks)
+        n = self.fp.readinto(b)
+        if not n:
+            # Ideally, we would raise IncompleteRead if the content-length
+            # wasn't satisfied, but it might break compatibility.
+            self._close_conn()
+        elif self.length is not None:
+            self.length -= n
+            if not self.length:
+                self._close_conn()
+        return n
+
+    def _read_next_chunk_size(self):
+        # Read the next chunk size from the file
+        line = self.fp.readline(_MAXLINE + 1)
+        if len(line) > _MAXLINE:
+            raise LineTooLong("chunk size")
+        i = line.find(b";")
+        if i >= 0:
+            line = line[:i]  # strip chunk-extensions
+        try:
+            return int(line, 16)
+        except ValueError:
+            # close the connection as protocol synchronisation is
+            # probably lost
+            self._close_conn()
+            raise
+
+    def _read_and_discard_trailer(self):
+        # read and discard trailer up to the CRLF terminator
+        ### note: we shouldn't have any trailers!
+        while True:
+            line = self.fp.readline(_MAXLINE + 1)
+            if len(line) > _MAXLINE:
+                raise LineTooLong("trailer line")
+            if not line:
+                # a vanishingly small number of sites EOF without
+                # sending the trailer
+                break
+            if line in (b"\r\n", b"\n", b""):
+                break
+
+    def _readall_chunked(self):
+        assert self.chunked != _UNKNOWN
+        chunk_left = self.chunk_left
+        value = []
+        while True:
+            if chunk_left is None:
+                try:
+                    chunk_left = self._read_next_chunk_size()
+                    if chunk_left == 0:
+                        break
+                except ValueError:
+                    raise IncompleteRead(b"".join(value))
+            value.append(self._safe_read(chunk_left))
+
+            # we read the whole chunk, get another
+            self._safe_read(2)  # toss the CRLF at the end of the chunk
+            chunk_left = None
+
+        self._read_and_discard_trailer()
+
+        # we read everything; close the "file"
+        self._close_conn()
+
+        return b"".join(value)
+
+    def _readinto_chunked(self, b):
+        assert self.chunked != _UNKNOWN
+        chunk_left = self.chunk_left
+
+        total_bytes = 0
+        mvb = memoryview(b)
+        while True:
+            if chunk_left is None:
+                try:
+                    chunk_left = self._read_next_chunk_size()
+                    if chunk_left == 0:
+                        break
+                except ValueError:
+                    raise IncompleteRead(bytes(b[0:total_bytes]))
+
+            if len(mvb) < chunk_left:
+                n = self._safe_readinto(mvb)
+                self.chunk_left = chunk_left - n
+                return total_bytes + n
+            elif len(mvb) == chunk_left:
+                n = self._safe_readinto(mvb)
+                self._safe_read(2)  # toss the CRLF at the end of the chunk
+                self.chunk_left = None
+                return total_bytes + n
+            else:
+                temp_mvb = mvb[0:chunk_left]
+                n = self._safe_readinto(temp_mvb)
+                mvb = mvb[n:]
+                total_bytes += n
+
+            # we read the whole chunk, get another
+            self._safe_read(2)  # toss the CRLF at the end of the chunk
+            chunk_left = None
+
+        self._read_and_discard_trailer()
+
+        # we read everything; close the "file"
+        self._close_conn()
+
+        return total_bytes
+
+    def _safe_read(self, amt):
+        """Read the number of bytes requested, compensating for partial reads.
+
+        Normally, we have a blocking socket, but a read() can be interrupted
+        by a signal (resulting in a partial read).
+
+        Note that we cannot distinguish between EOF and an interrupt when zero
+        bytes have been read. IncompleteRead() will be raised in this
+        situation.
+
+        This function should be used when <amt> bytes "should" be present for
+        reading. If the bytes are truly not available (due to EOF), then the
+        IncompleteRead exception can be used to detect the problem.
+        """
+        s = []
+        while amt > 0:
+            chunk = self.fp.read(min(amt, MAXAMOUNT))
+            if not chunk:
+                raise IncompleteRead(b"".join(s), amt)
+            s.append(chunk)
+            amt -= len(chunk)
+        return b"".join(s)
+
+    def _safe_readinto(self, b):
+        """Same as _safe_read, but for reading into a buffer."""
+        total_bytes = 0
+        mvb = memoryview(b)
+        while total_bytes < len(b):
+            if MAXAMOUNT < len(mvb):
+                temp_mvb = mvb[0:MAXAMOUNT]
+                n = self.fp.readinto(temp_mvb)
+            else:
+                n = self.fp.readinto(mvb)
+            if not n:
+                raise IncompleteRead(bytes(mvb[0:total_bytes]), len(b))
+            mvb = mvb[n:]
+            total_bytes += n
+        return total_bytes
+
+    def fileno(self):
+        return self.fp.fileno()
+
+    def getheader(self, name, default=None):
+        if self.headers is None:
+            raise ResponseNotReady()
+        headers = self.headers.get_all(name) or default
+        if isinstance(headers, str) or not hasattr(headers, "__iter__"):
+            return headers
+        else:
+            return ", ".join(headers)
+
+    def getheaders(self):
+        """Return list of (header, value) tuples."""
+        if self.headers is None:
+            raise ResponseNotReady()
+        return list(self.headers.items())
+
+    # We override IOBase.__iter__ so that it doesn't check for closed-ness
+
+    def __iter__(self):
+        return self
+
+    # For compatibility with old-style urllib responses.
+
+    def info(self):
+        return self.headers
+
+    def geturl(self):
+        return self.url
+
+    def getcode(self):
+        return self.status
+
+
+class HTTPConnection:
+    _http_vsn = 11
+    _http_vsn_str = "HTTP/1.1"
+
+    response_class = HTTPResponse
+    default_port = HTTP_PORT
+    auto_open = 1
+    debuglevel = 0
+
+    def __init__(
+        self,
+        host,
+        port=None,
+        strict=_strict_sentinel,
+        timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
+        source_address=None,
+    ):
+        if strict is not _strict_sentinel:
+            warnings.warn(
+                "the 'strict' argument isn't supported anymore; "
+                "http.client now always assumes HTTP/1.x compliant servers.",
+                DeprecationWarning,
+                2,
+            )
+        self.timeout = timeout
+        self.source_address = source_address
+        self.sock = None
+        self._buffer = []
+        self.__response = None
+        self.__state = _CS_IDLE
+        self._method = None
+        self._tunnel_host = None
+        self._tunnel_port = None
+        self._tunnel_headers = {}
+
+        self._set_hostport(host, port)
+
+    def set_tunnel(self, host, port=None, headers=None):
+        """Sets up the host and the port for the HTTP CONNECT Tunnelling.
+
+        The headers argument should be a mapping of extra HTTP headers
+        to send with the CONNECT request.
+        """
+        self._tunnel_host = host
+        self._tunnel_port = port
+        if headers:
+            self._tunnel_headers = headers
+        else:
+            self._tunnel_headers.clear()
+
+    def _set_hostport(self, host, port):
+        if port is None:
+            i = host.rfind(":")
+            j = host.rfind("]")  # ipv6 addresses have [...]
+            if i > j:
+                try:
+                    port = int(host[i + 1 :])
+                except ValueError:
+                    if host[i + 1 :] == "":  # http://foo.com:/ == http://foo.com/
+                        port = self.default_port
+                    else:
+                        raise InvalidURL("nonnumeric port: '%s'" % host[i + 1 :])
+                host = host[:i]
+            else:
+                port = self.default_port
+            if host and host[0] == "[" and host[-1] == "]":
+                host = host[1:-1]
+        self.host = host
+        self.port = port
+
+    def set_debuglevel(self, level):
+        self.debuglevel = level
+
+    def _tunnel(self):
+        self._set_hostport(self._tunnel_host, self._tunnel_port)
+        connect_str = "CONNECT %s:%d HTTP/1.0\r\n" % (self.host, self.port)
+        connect_bytes = connect_str.encode("ascii")
+        self.send(connect_bytes)
+        for header, value in self._tunnel_headers.items():
+            header_str = "%s: %s\r\n" % (header, value)
+            header_bytes = header_str.encode("latin-1")
+            self.send(header_bytes)
+        self.send(b"\r\n")
+
+        response = self.response_class(self.sock, method=self._method)
+        (version, code, message) = response._read_status()
+
+        if code != 200:
+            self.close()
+            raise socket.error("Tunnel connection failed: %d %s" % (code, message.strip()))
+        while True:
+            line = response.fp.readline(_MAXLINE + 1)
+            if len(line) > _MAXLINE:
+                raise LineTooLong("header line")
+            if not line:
+                # for sites which EOF without sending a trailer
+                break
+            if line in (b"\r\n", b"\n", b""):
+                break
+
+    def connect(self):
+        """Connect to the host and port specified in __init__."""
+        self.sock = socket.create_connection(
+            (self.host, self.port), self.timeout, self.source_address
+        )
+        if self._tunnel_host:
+            self._tunnel()
+
+    def close(self):
+        """Close the connection to the HTTP server."""
+        if self.sock:
+            self.sock.close()  # close it manually... there may be other refs
+            self.sock = None
+        if self.__response:
+            self.__response.close()
+            self.__response = None
+        self.__state = _CS_IDLE
+
+    def send(self, data):
+        """Send `data' to the server.
+        ``data`` can be a string object, a bytes object, an array object, a
+        file-like object that supports a .read() method, or an iterable object.
+        """
+
+        if self.sock is None:
+            if self.auto_open:
+                self.connect()
+            else:
+                raise NotConnected()
+
+        if self.debuglevel > 0:
+            print("send:", repr(data))
+        blocksize = 8192
+        if hasattr(data, "read"):
+            if self.debuglevel > 0:
+                print("sendIng a read()able")
+            encode = False
+            try:
+                mode = data.mode
+            except AttributeError:
+                # io.BytesIO and other file-like objects don't have a `mode`
+                # attribute.
+                pass
+            else:
+                if "b" not in mode:
+                    encode = True
+                    if self.debuglevel > 0:
+                        print("encoding file using iso-8859-1")
+            while 1:
+                datablock = data.read(blocksize)
+                if not datablock:
+                    break
+                if encode:
+                    datablock = datablock.encode("iso-8859-1")
+                self.sock.sendall(datablock)
+            return
+        try:
+            self.sock.sendall(data)
+        except TypeError:
+            if isinstance(data, collections.Iterable):
+                for d in data:
+                    self.sock.sendall(d)
+            else:
+                raise TypeError(
+                    "data should be a bytes-like object " "or an iterable, got %r" % type(data)
+                )
+
+    def _output(self, s):
+        """Add a line of output to the current request buffer.
+
+        Assumes that the line does *not* end with \\r\\n.
+        """
+        self._buffer.append(s)
+
+    def _send_output(self, message_body=None):
+        """Send the currently buffered request and clear the buffer.
+
+        Appends an extra \\r\\n to the buffer.
+        A message_body may be specified, to be appended to the request.
+        """
+        self._buffer.extend((b"", b""))
+        msg = b"\r\n".join(self._buffer)
+        del self._buffer[:]
+        # If msg and message_body are sent in a single send() call,
+        # it will avoid performance problems caused by the interaction
+        # between delayed ack and the Nagle algorithm.
+        if isinstance(message_body, bytes):
+            msg += message_body
+            message_body = None
+        self.send(msg)
+        if message_body is not None:
+            # message_body was not a string (i.e. it is a file), and
+            # we must run the risk of Nagle.
+            self.send(message_body)
+
+    def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0):
+        """Send a request to the server.
+
+        `method' specifies an HTTP request method, e.g. 'GET'.
+        `url' specifies the object being requested, e.g. '/index.html'.
+        `skip_host' if True does not add automatically a 'Host:' header
+        `skip_accept_encoding' if True does not add automatically an
+           'Accept-Encoding:' header
+        """
+
+        # if a prior response has been completed, then forget about it.
+        if self.__response and self.__response.isclosed():
+            self.__response = None
+
+        # in certain cases, we cannot issue another request on this connection.
+        # this occurs when:
+        #   1) we are in the process of sending a request.   (_CS_REQ_STARTED)
+        #   2) a response to a previous request has signalled that it is going
+        #      to close the connection upon completion.
+        #   3) the headers for the previous response have not been read, thus
+        #      we cannot determine whether point (2) is true.   (_CS_REQ_SENT)
+        #
+        # if there is no prior response, then we can request at will.
+        #
+        # if point (2) is true, then we will have passed the socket to the
+        # response (effectively meaning, "there is no prior response"), and
+        # will open a new one when a new request is made.
+        #
+        # Note: if a prior response exists, then we *can* start a new request.
+        #       We are not allowed to begin fetching the response to this new
+        #       request, however, until that prior response is complete.
+        #
+        if self.__state == _CS_IDLE:
+            self.__state = _CS_REQ_STARTED
+        else:
+            raise CannotSendRequest(self.__state)
+
+        # Save the method we use, we need it later in the response phase
+        self._method = method
+        if not url:
+            url = "/"
+        request = "%s %s %s" % (method, url, self._http_vsn_str)
+
+        # Non-ASCII characters should have been eliminated earlier
+        self._output(request.encode("ascii"))
+
+        if self._http_vsn == 11:
+            # Issue some standard headers for better HTTP/1.1 compliance
+
+            if not skip_host:
+                # this header is issued *only* for HTTP/1.1
+                # connections. more specifically, this means it is
+                # only issued when the client uses the new
+                # HTTPConnection() class. backwards-compat clients
+                # will be using HTTP/1.0 and those clients may be
+                # issuing this header themselves. we should NOT issue
+                # it twice; some web servers (such as Apache) barf
+                # when they see two Host: headers
+
+                # If we need a non-standard port,include it in the
+                # header.  If the request is going through a proxy,
+                # but the host of the actual URL, not the host of the
+                # proxy.
+
+                netloc = ""
+                if url.startswith("http"):
+                    nil, netloc, nil, nil, nil = urlsplit(url)
+
+                if netloc:
+                    try:
+                        netloc_enc = netloc.encode("ascii")
+                    except UnicodeEncodeError:
+                        netloc_enc = netloc.encode("idna")
+                    self.putheader("Host", netloc_enc)
+                else:
+                    try:
+                        host_enc = self.host.encode("ascii")
+                    except UnicodeEncodeError:
+                        host_enc = self.host.encode("idna")
+
+                    # As per RFC 273, IPv6 address should be wrapped with []
+                    # when used as Host header
+
+                    if self.host.find(":") >= 0:
+                        host_enc = b"[" + host_enc + b"]"
+
+                    if self.port == self.default_port:
+                        self.putheader("Host", host_enc)
+                    else:
+                        host_enc = host_enc.decode("ascii")
+                        self.putheader("Host", "%s:%s" % (host_enc, self.port))
+
+            # note: we are assuming that clients will not attempt to set these
+            #       headers since *this* library must deal with the
+            #       consequences. this also means that when the supporting
+            #       libraries are updated to recognize other forms, then this
+            #       code should be changed (removed or updated).
+
+            # we only want a Content-Encoding of "identity" since we don't
+            # support encodings such as x-gzip or x-deflate.
+            if not skip_accept_encoding:
+                self.putheader("Accept-Encoding", "identity")
+
+            # we can accept "chunked" Transfer-Encodings, but no others
+            # NOTE: no TE header implies *only* "chunked"
+            # self.putheader('TE', 'chunked')
+
+            # if TE is supplied in the header, then it must appear in a
+            # Connection header.
+            # self.putheader('Connection', 'TE')
+
+        else:
+            # For HTTP/1.0, the server will assume "not chunked"
+            pass
+
+    def putheader(self, header, *values):
+        """Send a request header line to the server.
+
+        For example: h.putheader('Accept', 'text/html')
+        """
+        if self.__state != _CS_REQ_STARTED:
+            raise CannotSendHeader()
+
+        if hasattr(header, "encode"):
+            header = header.encode("ascii")
+        values = list(values)
+        for i, one_value in enumerate(values):
+            if isinstance(one_value, str):
+                values[i] = one_value.encode("latin-1")
+            elif isinstance(one_value, int):
+                values[i] = str(one_value).encode("ascii")
+        value = b"\r\n\t".join(values)
+        header = header + b": " + value
+        self._output(header)
+
+    def endheaders(self, message_body=None):
+        """Indicate that the last header line has been sent to the server.
+
+        This method sends the request to the server.  The optional message_body
+        argument can be used to pass a message body associated with the
+        request.  The message body will be sent in the same packet as the
+        message headers if it is a string, otherwise it is sent as a separate
+        packet.
+        """
+        if self.__state == _CS_REQ_STARTED:
+            self.__state = _CS_REQ_SENT
+        else:
+            raise CannotSendHeader()
+        self._send_output(message_body)
+
+    def request(self, method, url, body=None, headers={}):
+        """Send a complete request to the server."""
+        self._send_request(method, url, body, headers)
+
+    def _set_content_length(self, body):
+        # Set the content-length based on the body.
+        thelen = None
+        try:
+            thelen = str(len(body))
+        except TypeError as te:
+            # If this is a file-like object, try to
+            # fstat its file descriptor
+            try:
+                thelen = str(os.fstat(body.fileno()).st_size)
+            except (AttributeError, OSError):
+                # Don't send a length if this failed
+                if self.debuglevel > 0:
+                    print("Cannot stat!!")
+
+        if thelen is not None:
+            self.putheader("Content-Length", thelen)
+
+    def _send_request(self, method, url, body, headers):
+        # Honor explicitly requested Host: and Accept-Encoding: headers.
+        header_names = dict.fromkeys([k.lower() for k in headers])
+        skips = {}
+        if "host" in header_names:
+            skips["skip_host"] = 1
+        if "accept-encoding" in header_names:
+            skips["skip_accept_encoding"] = 1
+
+        self.putrequest(method, url, **skips)
+
+        if body is not None and ("content-length" not in header_names):
+            self._set_content_length(body)
+        for hdr, value in headers.items():
+            self.putheader(hdr, value)
+        if isinstance(body, str):
+            # RFC 2616 Section 3.7.1 says that text default has a
+            # default charset of iso-8859-1.
+            body = body.encode("iso-8859-1")
+        self.endheaders(body)
+
+    def getresponse(self):
+        """Get the response from the server.
+
+        If the HTTPConnection is in the correct state, returns an
+        instance of HTTPResponse or of whatever object is returned by
+        class the response_class variable.
+
+        If a request has not been sent or if a previous response has
+        not be handled, ResponseNotReady is raised.  If the HTTP
+        response indicates that the connection should be closed, then
+        it will be closed before the response is returned.  When the
+        connection is closed, the underlying socket is closed.
+        """
+
+        # if a prior response has been completed, then forget about it.
+        if self.__response and self.__response.isclosed():
+            self.__response = None
+
+        # if a prior response exists, then it must be completed (otherwise, we
+        # cannot read this response's header to determine the connection-close
+        # behavior)
+        #
+        # note: if a prior response existed, but was connection-close, then the
+        # socket and response were made independent of this HTTPConnection
+        # object since a new request requires that we open a whole new
+        # connection
+        #
+        # this means the prior response had one of two states:
+        #   1) will_close: this connection was reset and the prior socket and
+        #                  response operate independently
+        #   2) persistent: the response was retained and we await its
+        #                  isclosed() status to become true.
+        #
+        if self.__state != _CS_REQ_SENT or self.__response:
+            raise ResponseNotReady(self.__state)
+
+        if self.debuglevel > 0:
+            response = self.response_class(self.sock, self.debuglevel, method=self._method)
+        else:
+            response = self.response_class(self.sock, method=self._method)
+
+        response.begin()
+        assert response.will_close != _UNKNOWN
+        self.__state = _CS_IDLE
+
+        if response.will_close:
+            # this effectively passes the connection to the response
+            self.close()
+        else:
+            # remember this, so we can tell when it is complete
+            self.__response = response
+
+        return response
+
+
+try:
+    import ssl
+except ImportError:
+    pass
+else:
+
+    class HTTPSConnection(HTTPConnection):
+        "This class allows communication via SSL."
+
+        default_port = HTTPS_PORT
+
+        # XXX Should key_file and cert_file be deprecated in favour of context?
+
+        def __init__(
+            self,
+            host,
+            port=None,
+            key_file=None,
+            cert_file=None,
+            strict=_strict_sentinel,
+            timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
+            source_address=None,
+            *,
+            context=None,
+            check_hostname=None
+        ):
+            super(HTTPSConnection, self).__init__(host, port, strict, timeout, source_address)
+            self.key_file = key_file
+            self.cert_file = cert_file
+            if context is None:
+                # Some reasonable defaults
+                context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+                context.options |= ssl.OP_NO_SSLv2
+            will_verify = context.verify_mode != ssl.CERT_NONE
+            if check_hostname is None:
+                check_hostname = will_verify
+            elif check_hostname and not will_verify:
+                raise ValueError(
+                    "check_hostname needs a SSL context with "
+                    "either CERT_OPTIONAL or CERT_REQUIRED"
+                )
+            if key_file or cert_file:
+                context.load_cert_chain(cert_file, key_file)
+            self._context = context
+            self._check_hostname = check_hostname
+
+        def connect(self):
+            "Connect to a host on a given (SSL) port."
+
+            sock = socket.create_connection(
+                (self.host, self.port), self.timeout, self.source_address
+            )
+
+            if self._tunnel_host:
+                self.sock = sock
+                self._tunnel()
+
+            server_hostname = self.host if ssl.HAS_SNI else None
+            self.sock = self._context.wrap_socket(sock, server_hostname=server_hostname)
+            try:
+                if self._check_hostname:
+                    ssl.match_hostname(self.sock.getpeercert(), self.host)
+            except Exception:
+                self.sock.shutdown(socket.SHUT_RDWR)
+                self.sock.close()
+                raise
+
+    __all__.append("HTTPSConnection")
+
+
+class HTTPException(Exception):
+    # Subclasses that define an __init__ must call Exception.__init__
+    # or define self.args.  Otherwise, str() will fail.
+    pass
+
+
+class NotConnected(HTTPException):
+    pass
+
+
+class InvalidURL(HTTPException):
+    pass
+
+
+class UnknownProtocol(HTTPException):
+    def __init__(self, version):
+        self.args = (version,)
+        self.version = version
+
+
+class UnknownTransferEncoding(HTTPException):
+    pass
+
+
+class UnimplementedFileMode(HTTPException):
+    pass
+
+
+class IncompleteRead(HTTPException):
+    def __init__(self, partial, expected=None):
+        self.args = (partial,)
+        self.partial = partial
+        self.expected = expected
+
+    def __repr__(self):
+        if self.expected is not None:
+            e = ", %i more expected" % self.expected
+        else:
+            e = ""
+        return "IncompleteRead(%i bytes read%s)" % (len(self.partial), e)
+
+    def __str__(self):
+        return repr(self)
+
+
+class ImproperConnectionState(HTTPException):
+    pass
+
+
+class CannotSendRequest(ImproperConnectionState):
+    pass
+
+
+class CannotSendHeader(ImproperConnectionState):
+    pass
+
+
+class ResponseNotReady(ImproperConnectionState):
+    pass
+
+
+class BadStatusLine(HTTPException):
+    def __init__(self, line):
+        if not line:
+            line = repr(line)
+        self.args = (line,)
+        self.line = line
+
+
+class LineTooLong(HTTPException):
+    def __init__(self, line_type):
+        HTTPException.__init__(
+            self, "got more than %d bytes when reading %s" % (_MAXLINE, line_type)
+        )
+
+
+# for backwards compatibility
+error = HTTPException
diff --git a/unix-ffi/http.client/manifest.py b/unix-ffi/http.client/manifest.py
new file mode 100644
index 000000000..add274422
--- /dev/null
+++ b/unix-ffi/http.client/manifest.py
@@ -0,0 +1,10 @@
+metadata(version="0.5.1")
+
+require("email.parser")
+require("email.message")
+require("socket")
+require("collections")
+require("urllib.parse")
+require("warnings")
+
+package("http")
diff --git a/unix-ffi/json/json/__init__.py b/unix-ffi/json/json/__init__.py
new file mode 100644
index 000000000..954618f33
--- /dev/null
+++ b/unix-ffi/json/json/__init__.py
@@ -0,0 +1,418 @@
+r"""JSON (JavaScript Object Notation) <http://json.org> is a subset of
+JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data
+interchange format.
+
+:mod:`json` exposes an API familiar to users of the standard library
+:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained
+version of the :mod:`json` library contained in Python 2.6, but maintains
+compatibility with Python 2.4 and Python 2.5 and (currently) has
+significant performance advantages, even without using the optional C
+extension for speedups.
+
+Encoding basic Python object hierarchies::
+
+    >>> import json
+    >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
+    '["foo", {"bar": ["baz", null, 1.0, 2]}]'
+    >>> print(json.dumps("\"foo\bar"))
+    "\"foo\bar"
+    >>> print(json.dumps('\u1234'))
+    "\u1234"
+    >>> print(json.dumps('\\'))
+    "\\"
+    >>> print(json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True))
+    {"a": 0, "b": 0, "c": 0}
+    >>> from io import StringIO
+    >>> io = StringIO()
+    >>> json.dump(['streaming API'], io)
+    >>> io.getvalue()
+    '["streaming API"]'
+
+Compact encoding::
+
+    >>> import json
+    >>> from collections import OrderedDict
+    >>> mydict = OrderedDict([('4', 5), ('6', 7)])
+    >>> json.dumps([1,2,3,mydict], separators=(',', ':'))
+    '[1,2,3,{"4":5,"6":7}]'
+
+Pretty printing::
+
+    >>> import json
+    >>> print(json.dumps({'4': 5, '6': 7}, sort_keys=True,
+    ...                  indent=4, separators=(',', ': ')))
+    {
+        "4": 5,
+        "6": 7
+    }
+
+Decoding JSON::
+
+    >>> import json
+    >>> obj = ['foo', {'bar': ['baz', None, 1.0, 2]}]
+    >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj
+    True
+    >>> json.loads('"\\"foo\\bar"') == '"foo\x08ar'
+    True
+    >>> from io import StringIO
+    >>> io = StringIO('["streaming API"]')
+    >>> json.load(io)[0] == 'streaming API'
+    True
+
+Specializing JSON object decoding::
+
+    >>> import json
+    >>> def as_complex(dct):
+    ...     if '__complex__' in dct:
+    ...         return complex(dct['real'], dct['imag'])
+    ...     return dct
+    ...
+    >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}',
+    ...     object_hook=as_complex)
+    (1+2j)
+    >>> from decimal import Decimal
+    >>> json.loads('1.1', parse_float=Decimal) == Decimal('1.1')
+    True
+
+Specializing JSON object encoding::
+
+    >>> import json
+    >>> def encode_complex(obj):
+    ...     if isinstance(obj, complex):
+    ...         return [obj.real, obj.imag]
+    ...     raise TypeError(repr(o) + " is not JSON serializable")
+    ...
+    >>> json.dumps(2 + 1j, default=encode_complex)
+    '[2.0, 1.0]'
+    >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j)
+    '[2.0, 1.0]'
+    >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j))
+    '[2.0, 1.0]'
+
+
+Using json.tool from the shell to validate and pretty-print::
+
+    $ echo '{"json":"obj"}' | python -m json.tool
+    {
+        "json": "obj"
+    }
+    $ echo '{ 1.2:3.4}' | python -m json.tool
+    Expecting property name enclosed in double quotes: line 1 column 3 (char 2)
+"""
+
+__all__ = [
+    "dump",
+    "dumps",
+    "load",
+    "loads",
+    "JSONDecoder",
+    "JSONEncoder",
+]
+
+__author__ = "Bob Ippolito <bob@redivi.com>"
+
+from .decoder import JSONDecoder
+from .encoder import JSONEncoder
+
+_default_encoder = JSONEncoder(
+    skipkeys=False,
+    ensure_ascii=True,
+    check_circular=True,
+    allow_nan=True,
+    indent=None,
+    separators=None,
+    default=None,
+)
+
+
+def dump(
+    obj,
+    fp,
+    skipkeys=False,
+    ensure_ascii=True,
+    check_circular=True,
+    allow_nan=True,
+    cls=None,
+    indent=None,
+    separators=None,
+    default=None,
+    sort_keys=False,
+    **kw
+):
+    """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a
+    ``.write()``-supporting file-like object).
+
+    If ``skipkeys`` is true then ``dict`` keys that are not basic types
+    (``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped
+    instead of raising a ``TypeError``.
+
+    If ``ensure_ascii`` is false, then the strings written to ``fp`` can
+    contain non-ASCII characters if they appear in strings contained in
+    ``obj``. Otherwise, all such characters are escaped in JSON strings.
+
+    If ``check_circular`` is false, then the circular reference check
+    for container types will be skipped and a circular reference will
+    result in an ``OverflowError`` (or worse).
+
+    If ``allow_nan`` is false, then it will be a ``ValueError`` to
+    serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``)
+    in strict compliance of the JSON specification, instead of using the
+    JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
+
+    If ``indent`` is a non-negative integer, then JSON array elements and
+    object members will be pretty-printed with that indent level. An indent
+    level of 0 will only insert newlines. ``None`` is the most compact
+    representation.  Since the default item separator is ``', '``,  the
+    output might include trailing whitespace when ``indent`` is specified.
+    You can use ``separators=(',', ': ')`` to avoid this.
+
+    If ``separators`` is an ``(item_separator, dict_separator)`` tuple
+    then it will be used instead of the default ``(', ', ': ')`` separators.
+    ``(',', ':')`` is the most compact JSON representation.
+
+    ``default(obj)`` is a function that should return a serializable version
+    of obj or raise TypeError. The default simply raises TypeError.
+
+    If *sort_keys* is ``True`` (default: ``False``), then the output of
+    dictionaries will be sorted by key.
+
+    To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the
+    ``.default()`` method to serialize additional types), specify it with
+    the ``cls`` kwarg; otherwise ``JSONEncoder`` is used.
+
+    """
+    # cached encoder
+    if (
+        not skipkeys
+        and ensure_ascii
+        and check_circular
+        and allow_nan
+        and cls is None
+        and indent is None
+        and separators is None
+        and default is None
+        and not sort_keys
+        and not kw
+    ):
+        iterable = _default_encoder.iterencode(obj)
+    else:
+        if cls is None:
+            cls = JSONEncoder
+        iterable = cls(
+            skipkeys=skipkeys,
+            ensure_ascii=ensure_ascii,
+            check_circular=check_circular,
+            allow_nan=allow_nan,
+            indent=indent,
+            separators=separators,
+            default=default,
+            sort_keys=sort_keys,
+            **kw
+        ).iterencode(obj)
+    # could accelerate with writelines in some versions of Python, at
+    # a debuggability cost
+    for chunk in iterable:
+        fp.write(chunk)
+
+
+def dumps(
+    obj,
+    skipkeys=False,
+    ensure_ascii=True,
+    check_circular=True,
+    allow_nan=True,
+    cls=None,
+    indent=None,
+    separators=None,
+    default=None,
+    sort_keys=False,
+    **kw
+):
+    """Serialize ``obj`` to a JSON formatted ``str``.
+
+    If ``skipkeys`` is false then ``dict`` keys that are not basic types
+    (``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped
+    instead of raising a ``TypeError``.
+
+    If ``ensure_ascii`` is false, then the return value can contain non-ASCII
+    characters if they appear in strings contained in ``obj``. Otherwise, all
+    such characters are escaped in JSON strings.
+
+    If ``check_circular`` is false, then the circular reference check
+    for container types will be skipped and a circular reference will
+    result in an ``OverflowError`` (or worse).
+
+    If ``allow_nan`` is false, then it will be a ``ValueError`` to
+    serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in
+    strict compliance of the JSON specification, instead of using the
+    JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
+
+    If ``indent`` is a non-negative integer, then JSON array elements and
+    object members will be pretty-printed with that indent level. An indent
+    level of 0 will only insert newlines. ``None`` is the most compact
+    representation.  Since the default item separator is ``', '``,  the
+    output might include trailing whitespace when ``indent`` is specified.
+    You can use ``separators=(',', ': ')`` to avoid this.
+
+    If ``separators`` is an ``(item_separator, dict_separator)`` tuple
+    then it will be used instead of the default ``(', ', ': ')`` separators.
+    ``(',', ':')`` is the most compact JSON representation.
+
+    ``default(obj)`` is a function that should return a serializable version
+    of obj or raise TypeError. The default simply raises TypeError.
+
+    If *sort_keys* is ``True`` (default: ``False``), then the output of
+    dictionaries will be sorted by key.
+
+    To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the
+    ``.default()`` method to serialize additional types), specify it with
+    the ``cls`` kwarg; otherwise ``JSONEncoder`` is used.
+
+    """
+    # cached encoder
+    if (
+        not skipkeys
+        and ensure_ascii
+        and check_circular
+        and allow_nan
+        and cls is None
+        and indent is None
+        and separators is None
+        and default is None
+        and not sort_keys
+        and not kw
+    ):
+        return _default_encoder.encode(obj)
+    if cls is None:
+        cls = JSONEncoder
+    return cls(
+        skipkeys=skipkeys,
+        ensure_ascii=ensure_ascii,
+        check_circular=check_circular,
+        allow_nan=allow_nan,
+        indent=indent,
+        separators=separators,
+        default=default,
+        sort_keys=sort_keys,
+        **kw
+    ).encode(obj)
+
+
+_default_decoder = JSONDecoder(object_hook=None, object_pairs_hook=None)
+
+
+def load(
+    fp,
+    cls=None,
+    object_hook=None,
+    parse_float=None,
+    parse_int=None,
+    parse_constant=None,
+    object_pairs_hook=None,
+    **kw
+):
+    """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing
+    a JSON document) to a Python object.
+
+    ``object_hook`` is an optional function that will be called with the
+    result of any object literal decode (a ``dict``). The return value of
+    ``object_hook`` will be used instead of the ``dict``. This feature
+    can be used to implement custom decoders (e.g. JSON-RPC class hinting).
+
+    ``object_pairs_hook`` is an optional function that will be called with the
+    result of any object literal decoded with an ordered list of pairs.  The
+    return value of ``object_pairs_hook`` will be used instead of the ``dict``.
+    This feature can be used to implement custom decoders that rely on the
+    order that the key and value pairs are decoded (for example,
+    collections.OrderedDict will remember the order of insertion). If
+    ``object_hook`` is also defined, the ``object_pairs_hook`` takes priority.
+
+    To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
+    kwarg; otherwise ``JSONDecoder`` is used.
+
+    """
+    return loads(
+        fp.read(),
+        cls=cls,
+        object_hook=object_hook,
+        parse_float=parse_float,
+        parse_int=parse_int,
+        parse_constant=parse_constant,
+        object_pairs_hook=object_pairs_hook,
+        **kw
+    )
+
+
+def loads(
+    s,
+    encoding=None,
+    cls=None,
+    object_hook=None,
+    parse_float=None,
+    parse_int=None,
+    parse_constant=None,
+    object_pairs_hook=None,
+    **kw
+):
+    """Deserialize ``s`` (a ``str``, ``bytes`` or ``bytearray`` instance
+    containing a JSON document) to a Python object.
+
+    ``object_hook`` is an optional function that will be called with the
+    result of any object literal decode (a ``dict``). The return value of
+    ``object_hook`` will be used instead of the ``dict``. This feature
+    can be used to implement custom decoders (e.g. JSON-RPC class hinting).
+
+    ``object_pairs_hook`` is an optional function that will be called with the
+    result of any object literal decoded with an ordered list of pairs.  The
+    return value of ``object_pairs_hook`` will be used instead of the ``dict``.
+    This feature can be used to implement custom decoders that rely on the
+    order that the key and value pairs are decoded (for example,
+    collections.OrderedDict will remember the order of insertion). If
+    ``object_hook`` is also defined, the ``object_pairs_hook`` takes priority.
+
+    ``parse_float``, if specified, will be called with the string
+    of every JSON float to be decoded. By default this is equivalent to
+    float(num_str). This can be used to use another datatype or parser
+    for JSON floats (e.g. decimal.Decimal).
+
+    ``parse_int``, if specified, will be called with the string
+    of every JSON int to be decoded. By default this is equivalent to
+    int(num_str). This can be used to use another datatype or parser
+    for JSON integers (e.g. float).
+
+    ``parse_constant``, if specified, will be called with one of the
+    following strings: -Infinity, Infinity, NaN, null, true, false.
+    This can be used to raise an exception if invalid JSON numbers
+    are encountered.
+
+    To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
+    kwarg; otherwise ``JSONDecoder`` is used.
+
+    The ``encoding`` argument is ignored and deprecated.
+
+    """
+    if (
+        cls is None
+        and object_hook is None
+        and parse_int is None
+        and parse_float is None
+        and parse_constant is None
+        and object_pairs_hook is None
+        and not kw
+    ):
+        return _default_decoder.decode(s)
+    if cls is None:
+        cls = JSONDecoder
+    if object_hook is not None:
+        kw["object_hook"] = object_hook
+    if object_pairs_hook is not None:
+        kw["object_pairs_hook"] = object_pairs_hook
+    if parse_float is not None:
+        kw["parse_float"] = parse_float
+    if parse_int is not None:
+        kw["parse_int"] = parse_int
+    if parse_constant is not None:
+        kw["parse_constant"] = parse_constant
+    if isinstance(s, (bytes, bytearray)):
+        s = s.decode('utf-8')
+    return cls(**kw).decode(s)
diff --git a/json/json/decoder.py b/unix-ffi/json/json/decoder.py
similarity index 80%
rename from json/json/decoder.py
rename to unix-ffi/json/json/decoder.py
index f4170c5f4..a232d6dca 100644
--- a/json/json/decoder.py
+++ b/unix-ffi/json/json/decoder.py
@@ -4,23 +4,24 @@
 import sys
 
 from json import scanner
+
 try:
     from _json import scanstring as c_scanstring
 except ImportError:
     c_scanstring = None
 
-__all__ = ['JSONDecoder']
+__all__ = ["JSONDecoder"]
 
 FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
 
-NaN, PosInf, NegInf = float('nan'), float('inf'), float('-inf')
+NaN, PosInf, NegInf = float("nan"), float("inf"), float("-inf")
 
 
 def linecol(doc, pos):
     if isinstance(doc, bytes):
-        newline = b'\n'
+        newline = b"\n"
     else:
-        newline = '\n'
+        newline = "\n"
     lineno = doc.count(newline, 0, pos) + 1
     if lineno == 1:
         colno = pos + 1
@@ -33,32 +34,38 @@ def errmsg(msg, doc, pos, end=None):
     # Note that this function is called from _json
     lineno, colno = linecol(doc, pos)
     if end is None:
-        fmt = '{0}: line {1} column {2} (char {3})'
+        fmt = "{0}: line {1} column {2} (char {3})"
         return fmt.format(msg, lineno, colno, pos)
-        #fmt = '%s: line %d column %d (char %d)'
-        #return fmt % (msg, lineno, colno, pos)
+        # fmt = '%s: line %d column %d (char %d)'
+        # return fmt % (msg, lineno, colno, pos)
     endlineno, endcolno = linecol(doc, end)
-    fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})'
+    fmt = "{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})"
     return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end)
-    #fmt = '%s: line %d column %d - line %d column %d (char %d - %d)'
-    #return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end)
+    # fmt = '%s: line %d column %d - line %d column %d (char %d - %d)'
+    # return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end)
 
 
 _CONSTANTS = {
-    '-Infinity': NegInf,
-    'Infinity': PosInf,
-    'NaN': NaN,
+    "-Infinity": NegInf,
+    "Infinity": PosInf,
+    "NaN": NaN,
 }
 
 
 STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS)
 BACKSLASH = {
-    '"': '"', '\\': '\\', '/': '/',
-    'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t',
+    '"': '"',
+    "\\": "\\",
+    "/": "/",
+    "b": "\b",
+    "f": "\f",
+    "n": "\n",
+    "r": "\r",
+    "t": "\t",
 }
 
-def py_scanstring(s, end, strict=True,
-        _b=BACKSLASH, _m=STRINGCHUNK.match):
+
+def py_scanstring(s, end, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match):
     """Scan the string s for a JSON string. End is the index of the
     character in s after the quote that started the JSON string.
     Unescapes all valid JSON string escape sequences and raises ValueError
@@ -73,8 +80,7 @@ def py_scanstring(s, end, strict=True,
     while 1:
         chunk = _m(s, end)
         if chunk is None:
-            raise ValueError(
-                errmsg("Unterminated string starting at", s, begin))
+            raise ValueError(errmsg("Unterminated string starting at", s, begin))
         end = chunk.end()
         content, terminator = chunk.groups()
         # Content is contains zero or more unescaped string characters
@@ -84,9 +90,9 @@ def py_scanstring(s, end, strict=True,
         # or a backslash denoting that an escape sequence follows
         if terminator == '"':
             break
-        elif terminator != '\\':
+        elif terminator != "\\":
             if strict:
-                #msg = "Invalid control character %r at" % (terminator,)
+                # msg = "Invalid control character %r at" % (terminator,)
                 msg = "Invalid control character {0!r} at".format(terminator)
                 raise ValueError(errmsg(msg, s, end))
             else:
@@ -95,10 +101,9 @@ def py_scanstring(s, end, strict=True,
         try:
             esc = s[end]
         except IndexError:
-            raise ValueError(
-                errmsg("Unterminated string starting at", s, begin))
+            raise ValueError(errmsg("Unterminated string starting at", s, begin))
         # If not a unicode escape sequence, must be in the lookup table
-        if esc != 'u':
+        if esc != "u":
             try:
                 char = _b[esc]
             except KeyError:
@@ -106,38 +111,46 @@ def py_scanstring(s, end, strict=True,
                 raise ValueError(errmsg(msg, s, end))
             end += 1
         else:
-            esc = s[end + 1:end + 5]
+            esc = s[end + 1 : end + 5]
             next_end = end + 5
             if len(esc) != 4:
                 msg = "Invalid \\uXXXX escape"
                 raise ValueError(errmsg(msg, s, end))
             uni = int(esc, 16)
-            if 0xd800 <= uni <= 0xdbff:
+            if 0xD800 <= uni <= 0xDBFF:
                 msg = "Invalid \\uXXXX\\uXXXX surrogate pair"
-                if not s[end + 5:end + 7] == '\\u':
+                if not s[end + 5 : end + 7] == "\\u":
                     raise ValueError(errmsg(msg, s, end))
-                esc2 = s[end + 7:end + 11]
+                esc2 = s[end + 7 : end + 11]
                 if len(esc2) != 4:
                     raise ValueError(errmsg(msg, s, end))
                 uni2 = int(esc2, 16)
-                uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00))
+                uni = 0x10000 + (((uni - 0xD800) << 10) | (uni2 - 0xDC00))
                 next_end += 6
             char = chr(uni)
 
             end = next_end
         _append(char)
-    return ''.join(chunks), end
+    return "".join(chunks), end
 
 
 # Use speedup if available
 scanstring = c_scanstring or py_scanstring
 
-WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)
-WHITESPACE_STR = ' \t\n\r'
+WHITESPACE = re.compile(r"[ \t\n\r]*", FLAGS)
+WHITESPACE_STR = " \t\n\r"
 
 
-def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
-               memo=None, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
+def JSONObject(
+    s_and_end,
+    strict,
+    scan_once,
+    object_hook,
+    object_pairs_hook,
+    memo=None,
+    _w=WHITESPACE.match,
+    _ws=WHITESPACE_STR,
+):
     s, end = s_and_end
     pairs = []
     pairs_append = pairs.append
@@ -147,14 +160,14 @@ def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
     memo_get = memo.setdefault
     # Use a slice to prevent IndexError from being raised, the following
     # check will raise a more specific ValueError if the string is empty
-    nextchar = s[end:end + 1]
+    nextchar = s[end : end + 1]
     # Normally we expect nextchar == '"'
     if nextchar != '"':
         if nextchar in _ws:
             end = _w(s, end).end()
-            nextchar = s[end:end + 1]
+            nextchar = s[end : end + 1]
         # Trivial empty object
-        if nextchar == '}':
+        if nextchar == "}":
             if object_pairs_hook is not None:
                 result = object_pairs_hook(pairs)
                 return result, end + 1
@@ -163,17 +176,16 @@ def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
                 pairs = object_hook(pairs)
             return pairs, end + 1
         elif nextchar != '"':
-            raise ValueError(errmsg(
-                "Expecting property name enclosed in double quotes", s, end))
+            raise ValueError(errmsg("Expecting property name enclosed in double quotes", s, end))
     end += 1
     while True:
         key, end = scanstring(s, end, strict)
         key = memo_get(key, key)
         # To skip some function call overhead we optimize the fast paths where
         # the JSON key separator is ": " or just ":".
-        if s[end:end + 1] != ':':
+        if s[end : end + 1] != ":":
             end = _w(s, end).end()
-            if s[end:end + 1] != ':':
+            if s[end : end + 1] != ":":
                 raise ValueError(errmsg("Expecting ':' delimiter", s, end))
         end += 1
 
@@ -196,19 +208,20 @@ def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
                 end = _w(s, end + 1).end()
                 nextchar = s[end]
         except IndexError:
-            nextchar = ''
+            nextchar = ""
         end += 1
 
-        if nextchar == '}':
+        if nextchar == "}":
             break
-        elif nextchar != ',':
+        elif nextchar != ",":
             raise ValueError(errmsg("Expecting ',' delimiter", s, end - 1))
         end = _w(s, end).end()
-        nextchar = s[end:end + 1]
+        nextchar = s[end : end + 1]
         end += 1
         if nextchar != '"':
-            raise ValueError(errmsg(
-                "Expecting property name enclosed in double quotes", s, end - 1))
+            raise ValueError(
+                errmsg("Expecting property name enclosed in double quotes", s, end - 1)
+            )
     if object_pairs_hook is not None:
         result = object_pairs_hook(pairs)
         return result, end
@@ -217,15 +230,16 @@ def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
         pairs = object_hook(pairs)
     return pairs, end
 
+
 def JSONArray(s_and_end, scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
     s, end = s_and_end
     values = []
-    nextchar = s[end:end + 1]
+    nextchar = s[end : end + 1]
     if nextchar in _ws:
         end = _w(s, end + 1).end()
-        nextchar = s[end:end + 1]
+        nextchar = s[end : end + 1]
     # Look-ahead for trivial empty array
-    if nextchar == ']':
+    if nextchar == "]":
         return values, end + 1
     _append = values.append
     while True:
@@ -234,14 +248,14 @@ def JSONArray(s_and_end, scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
         except StopIteration:
             raise ValueError(errmsg("Expecting object", s, end))
         _append(value)
-        nextchar = s[end:end + 1]
+        nextchar = s[end : end + 1]
         if nextchar in _ws:
             end = _w(s, end + 1).end()
-            nextchar = s[end:end + 1]
+            nextchar = s[end : end + 1]
         end += 1
-        if nextchar == ']':
+        if nextchar == "]":
             break
-        elif nextchar != ',':
+        elif nextchar != ",":
             raise ValueError(errmsg("Expecting ',' delimiter", s, end))
         try:
             if s[end] in _ws:
@@ -284,9 +298,15 @@ class JSONDecoder(object):
 
     """
 
-    def __init__(self, object_hook=None, parse_float=None,
-            parse_int=None, parse_constant=None, strict=True,
-            object_pairs_hook=None):
+    def __init__(
+        self,
+        object_hook=None,
+        parse_float=None,
+        parse_int=None,
+        parse_constant=None,
+        strict=True,
+        object_pairs_hook=None,
+    ):
         """``object_hook``, if specified, will be called with the result
         of every JSON object decoded and its return value will be used in
         place of the given ``dict``.  This can be used to provide custom
@@ -334,7 +354,6 @@ def __init__(self, object_hook=None, parse_float=None,
         self.memo = {}
         self.scan_once = scanner.make_scanner(self)
 
-
     def decode(self, s, _w=WHITESPACE.match):
         """Return the Python representation of ``s`` (a ``str`` instance
         containing a JSON document).
diff --git a/json/json/encoder.py b/unix-ffi/json/json/encoder.py
similarity index 79%
rename from json/json/encoder.py
rename to unix-ffi/json/json/encoder.py
index 1d8b20c09..2adf366e3 100644
--- a/json/json/encoder.py
+++ b/unix-ffi/json/json/encoder.py
@@ -13,36 +13,36 @@
 
 ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]')
 ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
-HAS_UTF8 = re.compile(b'[\x80-\xff]')
+HAS_UTF8 = re.compile(b"[\x80-\xff]")
 ESCAPE_DCT = {
-    '\\': '\\\\',
+    "\\": "\\\\",
     '"': '\\"',
-    '\b': '\\b',
-    '\f': '\\f',
-    '\n': '\\n',
-    '\r': '\\r',
-    '\t': '\\t',
+    "\b": "\\b",
+    "\f": "\\f",
+    "\n": "\\n",
+    "\r": "\\r",
+    "\t": "\\t",
 }
 for i in range(0x20):
-    ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i))
-    #ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,))
+    ESCAPE_DCT.setdefault(chr(i), "\\u{0:04x}".format(i))
+    # ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,))
 
-INFINITY = float('inf')
+INFINITY = float("inf")
 FLOAT_REPR = repr
 
+
 def encode_basestring(s):
-    """Return a JSON representation of a Python string
+    """Return a JSON representation of a Python string"""
 
-    """
     def replace(match):
         return ESCAPE_DCT[match.group(0)]
+
     return '"' + ESCAPE.sub(replace, s) + '"'
 
 
 def py_encode_basestring_ascii(s):
-    """Return an ASCII-only JSON representation of a Python string
+    """Return an ASCII-only JSON representation of a Python string"""
 
-    """
     def replace(match):
         s = match.group(0)
         try:
@@ -50,19 +50,20 @@ def replace(match):
         except KeyError:
             n = ord(s)
             if n < 0x10000:
-                return '\\u{0:04x}'.format(n)
-                #return '\\u%04x' % (n,)
+                return "\\u{0:04x}".format(n)
+                # return '\\u%04x' % (n,)
             else:
                 # surrogate pair
                 n -= 0x10000
-                s1 = 0xd800 | ((n >> 10) & 0x3ff)
-                s2 = 0xdc00 | (n & 0x3ff)
-                return '\\u{0:04x}\\u{1:04x}'.format(s1, s2)
+                s1 = 0xD800 | ((n >> 10) & 0x3FF)
+                s2 = 0xDC00 | (n & 0x3FF)
+                return "\\u{0:04x}\\u{1:04x}".format(s1, s2)
+
     return '"' + ESCAPE_ASCII.sub(replace, s) + '"'
 
 
-encode_basestring_ascii = (
-    c_encode_basestring_ascii or py_encode_basestring_ascii)
+encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii
+
 
 class JSONEncoder(object):
     """Extensible JSON <http://json.org> encoder for Python data structures.
@@ -93,11 +94,21 @@ class JSONEncoder(object):
     implementation (to raise ``TypeError``).
 
     """
-    item_separator = ', '
-    key_separator = ': '
-    def __init__(self, skipkeys=False, ensure_ascii=True,
-            check_circular=True, allow_nan=True, sort_keys=False,
-            indent=None, separators=None, default=None):
+
+    item_separator = ", "
+    key_separator = ": "
+
+    def __init__(
+        self,
+        skipkeys=False,
+        ensure_ascii=True,
+        check_circular=True,
+        allow_nan=True,
+        sort_keys=False,
+        indent=None,
+        separators=None,
+        default=None,
+    ):
         """Constructor for JSONEncoder, with sensible defaults.
 
         If skipkeys is false, then it is a TypeError to attempt
@@ -191,7 +202,7 @@ def encode(self, o):
         chunks = self.iterencode(o, _one_shot=True)
         if not isinstance(chunks, (list, tuple)):
             chunks = list(chunks)
-        return ''.join(chunks)
+        return "".join(chunks)
 
     def iterencode(self, o, _one_shot=False):
         """Encode the given object and yield each string
@@ -212,72 +223,93 @@ def iterencode(self, o, _one_shot=False):
         else:
             _encoder = encode_basestring
 
-        def floatstr(o, allow_nan=self.allow_nan,
-                _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY):
+        def floatstr(
+            o, allow_nan=self.allow_nan, _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY
+        ):
             # Check for specials.  Note that this type of test is processor
             # and/or platform-specific, so do tests which don't depend on the
             # internals.
 
             if o != o:
-                text = 'NaN'
+                text = "NaN"
             elif o == _inf:
-                text = 'Infinity'
+                text = "Infinity"
             elif o == _neginf:
-                text = '-Infinity'
+                text = "-Infinity"
             else:
                 return _repr(o)
 
             if not allow_nan:
-                raise ValueError(
-                    "Out of range float values are not JSON compliant: " +
-                    repr(o))
+                raise ValueError("Out of range float values are not JSON compliant: " + repr(o))
 
             return text
 
-
-        if (_one_shot and c_make_encoder is not None
-                and self.indent is None):
+        if _one_shot and c_make_encoder is not None and self.indent is None:
             _iterencode = c_make_encoder(
-                markers, self.default, _encoder, self.indent,
-                self.key_separator, self.item_separator, self.sort_keys,
-                self.skipkeys, self.allow_nan)
+                markers,
+                self.default,
+                _encoder,
+                self.indent,
+                self.key_separator,
+                self.item_separator,
+                self.sort_keys,
+                self.skipkeys,
+                self.allow_nan,
+            )
         else:
             _iterencode = _make_iterencode(
-                markers, self.default, _encoder, self.indent, floatstr,
-                self.key_separator, self.item_separator, self.sort_keys,
-                self.skipkeys, _one_shot)
+                markers,
+                self.default,
+                _encoder,
+                self.indent,
+                floatstr,
+                self.key_separator,
+                self.item_separator,
+                self.sort_keys,
+                self.skipkeys,
+                _one_shot,
+            )
         return _iterencode(o, 0)
 
-def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
-        _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot,
-        ## HACK: hand-optimized bytecode; turn globals into locals
-        ValueError=ValueError,
-        dict=dict,
-        float=float,
-        id=id,
-        int=int,
-        isinstance=isinstance,
-        list=list,
-        str=str,
-        tuple=tuple,
-    ):
 
+def _make_iterencode(
+    markers,
+    _default,
+    _encoder,
+    _indent,
+    _floatstr,
+    _key_separator,
+    _item_separator,
+    _sort_keys,
+    _skipkeys,
+    _one_shot,
+    ## HACK: hand-optimized bytecode; turn globals into locals
+    ValueError=ValueError,
+    dict=dict,
+    float=float,
+    id=id,
+    int=int,
+    isinstance=isinstance,
+    list=list,
+    str=str,
+    tuple=tuple,
+):
     if _indent is not None and not isinstance(_indent, str):
-        _indent = ' ' * _indent
+        _indent = " " * _indent
 
     def _iterencode_list(lst, _current_indent_level):
         if not lst:
-            yield '[]'
+            yield "[]"
             return
         if markers is not None:
             markerid = id(lst)
             if markerid in markers:
                 raise ValueError("Circular reference detected")
             markers[markerid] = lst
-        buf = '['
+        buf = "["
         if _indent is not None:
             _current_indent_level += 1
-            newline_indent = '\n' + _indent * _current_indent_level
+            newline_indent = "\n" + _indent * _current_indent_level
             separator = _item_separator + newline_indent
             buf += newline_indent
         else:
@@ -292,11 +324,11 @@ def _iterencode_list(lst, _current_indent_level):
             if isinstance(value, str):
                 yield buf + _encoder(value)
             elif value is None:
-                yield buf + 'null'
+                yield buf + "null"
             elif value is True:
-                yield buf + 'true'
+                yield buf + "true"
             elif value is False:
-                yield buf + 'false'
+                yield buf + "false"
             elif isinstance(value, int):
                 yield buf + str(value)
             elif isinstance(value, float):
@@ -313,24 +345,24 @@ def _iterencode_list(lst, _current_indent_level):
                     yield chunk
         if newline_indent is not None:
             _current_indent_level -= 1
-            yield '\n' + _indent * _current_indent_level
-        yield ']'
+            yield "\n" + _indent * _current_indent_level
+        yield "]"
         if markers is not None:
             del markers[markerid]
 
     def _iterencode_dict(dct, _current_indent_level):
         if not dct:
-            yield '{}'
+            yield "{}"
             return
         if markers is not None:
             markerid = id(dct)
             if markerid in markers:
                 raise ValueError("Circular reference detected")
             markers[markerid] = dct
-        yield '{'
+        yield "{"
         if _indent is not None:
             _current_indent_level += 1
-            newline_indent = '\n' + _indent * _current_indent_level
+            newline_indent = "\n" + _indent * _current_indent_level
             item_separator = _item_separator + newline_indent
             yield newline_indent
         else:
@@ -349,11 +381,11 @@ def _iterencode_dict(dct, _current_indent_level):
             elif isinstance(key, float):
                 key = _floatstr(key)
             elif key is True:
-                key = 'true'
+                key = "true"
             elif key is False:
-                key = 'false'
+                key = "false"
             elif key is None:
-                key = 'null'
+                key = "null"
             elif isinstance(key, int):
                 key = str(key)
             elif _skipkeys:
@@ -369,11 +401,11 @@ def _iterencode_dict(dct, _current_indent_level):
             if isinstance(value, str):
                 yield _encoder(value)
             elif value is None:
-                yield 'null'
+                yield "null"
             elif value is True:
-                yield 'true'
+                yield "true"
             elif value is False:
-                yield 'false'
+                yield "false"
             elif isinstance(value, int):
                 yield str(value)
             elif isinstance(value, float):
@@ -389,8 +421,8 @@ def _iterencode_dict(dct, _current_indent_level):
                     yield chunk
         if newline_indent is not None:
             _current_indent_level -= 1
-            yield '\n' + _indent * _current_indent_level
-        yield '}'
+            yield "\n" + _indent * _current_indent_level
+        yield "}"
         if markers is not None:
             del markers[markerid]
 
@@ -398,11 +430,11 @@ def _iterencode(o, _current_indent_level):
         if isinstance(o, str):
             yield _encoder(o)
         elif o is None:
-            yield 'null'
+            yield "null"
         elif o is True:
-            yield 'true'
+            yield "true"
         elif o is False:
-            yield 'false'
+            yield "false"
         elif isinstance(o, int):
             yield str(o)
         elif isinstance(o, float):
@@ -424,4 +456,5 @@ def _iterencode(o, _current_indent_level):
                 yield chunk
             if markers is not None:
                 del markers[markerid]
+
     return _iterencode
diff --git a/unix-ffi/json/json/scanner.py b/unix-ffi/json/json/scanner.py
new file mode 100644
index 000000000..5ddf305b3
--- /dev/null
+++ b/unix-ffi/json/json/scanner.py
@@ -0,0 +1,77 @@
+"""JSON token scanner
+"""
+import re
+
+try:
+    from _json import make_scanner as c_make_scanner
+except ImportError:
+    c_make_scanner = None
+
+__all__ = ["make_scanner"]
+
+NUMBER_RE = re.compile(
+    r"(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?", (re.VERBOSE | re.MULTILINE | re.DOTALL)
+)
+
+
+def py_make_scanner(context):
+    parse_object = context.parse_object
+    parse_array = context.parse_array
+    parse_string = context.parse_string
+    match_number = NUMBER_RE.match
+    strict = context.strict
+    parse_float = context.parse_float
+    parse_int = context.parse_int
+    parse_constant = context.parse_constant
+    object_hook = context.object_hook
+    object_pairs_hook = context.object_pairs_hook
+    memo = context.memo
+
+    def _scan_once(string, idx):
+        try:
+            nextchar = string[idx]
+        except IndexError:
+            raise StopIteration
+
+        if nextchar == '"':
+            return parse_string(string, idx + 1, strict)
+        elif nextchar == "{":
+            return parse_object(
+                (string, idx + 1), strict, _scan_once, object_hook, object_pairs_hook, memo
+            )
+        elif nextchar == "[":
+            return parse_array((string, idx + 1), _scan_once)
+        elif nextchar == "n" and string[idx : idx + 4] == "null":
+            return None, idx + 4
+        elif nextchar == "t" and string[idx : idx + 4] == "true":
+            return True, idx + 4
+        elif nextchar == "f" and string[idx : idx + 5] == "false":
+            return False, idx + 5
+
+        m = match_number(string, idx)
+        if m is not None:
+            integer, frac, exp = m.groups()
+            if frac or exp:
+                res = parse_float(integer + (frac or "") + (exp or ""))
+            else:
+                res = parse_int(integer)
+            return res, m.end()
+        elif nextchar == "N" and string[idx : idx + 3] == "NaN":
+            return parse_constant("NaN"), idx + 3
+        elif nextchar == "I" and string[idx : idx + 8] == "Infinity":
+            return parse_constant("Infinity"), idx + 8
+        elif nextchar == "-" and string[idx : idx + 9] == "-Infinity":
+            return parse_constant("-Infinity"), idx + 9
+        else:
+            raise StopIteration
+
+    def scan_once(string, idx):
+        try:
+            return _scan_once(string, idx)
+        finally:
+            memo.clear()
+
+    return _scan_once
+
+
+make_scanner = c_make_scanner or py_make_scanner
diff --git a/unix-ffi/json/json/tool.py b/unix-ffi/json/json/tool.py
new file mode 100644
index 000000000..f88278b33
--- /dev/null
+++ b/unix-ffi/json/json/tool.py
@@ -0,0 +1,40 @@
+r"""Command-line tool to validate and pretty-print JSON
+
+Usage::
+
+    $ echo '{"json":"obj"}' | python -m json.tool
+    {
+        "json": "obj"
+    }
+    $ echo '{ 1.2:3.4}' | python -m json.tool
+    Expecting property name enclosed in double quotes: line 1 column 3 (char 2)
+
+"""
+import sys
+import json
+
+
+def main():
+    if len(sys.argv) == 1:
+        infile = sys.stdin
+        outfile = sys.stdout
+    elif len(sys.argv) == 2:
+        infile = open(sys.argv[1], "r")
+        outfile = sys.stdout
+    elif len(sys.argv) == 3:
+        infile = open(sys.argv[1], "r")
+        outfile = open(sys.argv[2], "w")
+    else:
+        raise SystemExit(sys.argv[0] + " [infile [outfile]]")
+    with infile:
+        try:
+            obj = json.load(infile)
+        except ValueError as e:
+            raise SystemExit(e)
+    with outfile:
+        json.dump(obj, outfile, sort_keys=True, indent=4, separators=(",", ": "))
+        outfile.write("\n")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/unix-ffi/json/manifest.py b/unix-ffi/json/manifest.py
new file mode 100644
index 000000000..9267719f1
--- /dev/null
+++ b/unix-ffi/json/manifest.py
@@ -0,0 +1,4 @@
+metadata(version="0.2.0")
+
+require("re")
+package("json")
diff --git a/unix-ffi/json/test_json.py b/unix-ffi/json/test_json.py
new file mode 100644
index 000000000..7d7ba2104
--- /dev/null
+++ b/unix-ffi/json/test_json.py
@@ -0,0 +1,13 @@
+import json
+
+inp = ["foo", {"bar": ("baz", None, 1, 2)}]
+print(inp)
+
+s = json.dumps(inp)
+print(s)
+
+outp = json.loads(s)
+print(outp)
+
+# Doesn't work because JSON doesn't have tuples
+# assert inp == outp
diff --git a/machine/example_timer.py b/unix-ffi/machine/example_timer.py
similarity index 85%
rename from machine/example_timer.py
rename to unix-ffi/machine/example_timer.py
index a0d44110f..550d68cd3 100644
--- a/machine/example_timer.py
+++ b/unix-ffi/machine/example_timer.py
@@ -1,4 +1,4 @@
-import utime
+import time
 from machine import Timer
 
 
@@ -7,5 +7,5 @@
 t1.callback(lambda t: print(t, "tick1"))
 t2.callback(lambda t: print(t, "tick2"))
 
-utime.sleep(3)
+time.sleep(3)
 print("done")
diff --git a/unix-ffi/machine/machine/__init__.py b/unix-ffi/machine/machine/__init__.py
new file mode 100644
index 000000000..80bf5da1a
--- /dev/null
+++ b/unix-ffi/machine/machine/__init__.py
@@ -0,0 +1,7 @@
+from umachine import *
+from .timer import *
+from .pin import *
+
+
+def unique_id():
+    return b"upy-non-unique"
diff --git a/unix-ffi/machine/machine/pin.py b/unix-ffi/machine/machine/pin.py
new file mode 100644
index 000000000..c7d827be5
--- /dev/null
+++ b/unix-ffi/machine/machine/pin.py
@@ -0,0 +1,28 @@
+import umachine
+
+
+class Pin(umachine.PinBase):
+    IN = "in"
+    OUT = "out"
+
+    def __init__(self, no, dir=IN):
+        pref = "/sys/class/gpio/gpio{}/".format(no)
+        dirf = pref + "direction"
+        try:
+            f = open(dirf, "w")
+        except OSError:
+            with open("/sys/class/gpio/export", "w") as f:
+                f.write(str(no))
+            f = open(dirf, "w")
+        f.write(dir)
+        f.close()
+        self.f = open(pref + "value", "r+b")
+
+    def value(self, v=None):
+        if v is None:
+            self.f.seek(0)
+            return 1 if self.f.read(1) == b"1" else 0
+        self.f.write(b"1" if v else b"0")
+
+    def deinit(self):
+        self.f.close()
diff --git a/machine/machine/timer.py b/unix-ffi/machine/machine/timer.py
similarity index 80%
rename from machine/machine/timer.py
rename to unix-ffi/machine/machine/timer.py
index ecd614041..be00cee33 100644
--- a/machine/machine/timer.py
+++ b/unix-ffi/machine/machine/timer.py
@@ -1,13 +1,14 @@
 import ffilib
 import uctypes
 import array
-import uos
 import os
-import utime
 from signal import *
 
 libc = ffilib.libc()
-librt = ffilib.open("librt")
+try:
+    librt = ffilib.open("librt")
+except OSError as e:
+    librt = libc
 
 CLOCK_REALTIME = 0
 CLOCK_MONOTONIC = 1
@@ -41,38 +42,40 @@
 timer_create_ = librt.func("i", "timer_create", "ipp")
 timer_settime_ = librt.func("i", "timer_settime", "PiPp")
 
+
 def new(sdesc):
     buf = bytearray(uctypes.sizeof(sdesc))
     s = uctypes.struct(uctypes.addressof(buf), sdesc, uctypes.NATIVE)
     return s
 
+
 def timer_create(sig_id):
     sev = new(sigevent_t)
-    #print(sev)
+    # print(sev)
     sev.sigev_notify = SIGEV_SIGNAL
     sev.sigev_signo = SIGRTMIN + sig_id
-    timerid = array.array('P', [0])
+    timerid = array.array("P", [0])
     r = timer_create_(CLOCK_MONOTONIC, sev, timerid)
     os.check_error(r)
-    #print("timerid", hex(timerid[0]))
+    # print("timerid", hex(timerid[0]))
     return timerid[0]
 
+
 def timer_settime(tid, hz):
     period = 1000000000 // hz
     new_val = new(itimerspec_t)
     new_val.it_value.tv_nsec = period
     new_val.it_interval.tv_nsec = period
-    #print("new_val:", bytes(new_val))
+    # print("new_val:", bytes(new_val))
     old_val = new(itimerspec_t)
-    #print(new_val, old_val)
+    # print(new_val, old_val)
     r = timer_settime_(tid, 0, new_val, old_val)
     os.check_error(r)
-    #print("old_val:", bytes(old_val))
-    #print("timer_settime", r)
+    # print("old_val:", bytes(old_val))
+    # print("timer_settime", r)
 
 
 class Timer:
-
     def __init__(self, id, freq):
         self.id = id
         self.tid = timer_create(id)
@@ -82,8 +85,8 @@ def callback(self, cb):
         self.cb = cb
         timer_settime(self.tid, self.freq)
         org_sig = signal(SIGRTMIN + self.id, self.handler)
-        #print("Sig %d: %s" % (SIGRTMIN + self.id, org_sig))
+        # print("Sig %d: %s" % (SIGRTMIN + self.id, org_sig))
 
     def handler(self, signum):
-        #print('Signal handler called with signal', signum)
+        # print('Signal handler called with signal', signum)
         self.cb(self)
diff --git a/unix-ffi/machine/manifest.py b/unix-ffi/machine/manifest.py
new file mode 100644
index 000000000..f7c11b81a
--- /dev/null
+++ b/unix-ffi/machine/manifest.py
@@ -0,0 +1,9 @@
+metadata(version="0.2.2")
+
+# Originally written by Paul Sokolovsky.
+
+require("ffilib")
+require("os")
+require("signal")
+
+package("machine")
diff --git a/unix-ffi/multiprocessing/manifest.py b/unix-ffi/multiprocessing/manifest.py
new file mode 100644
index 000000000..d6b32411d
--- /dev/null
+++ b/unix-ffi/multiprocessing/manifest.py
@@ -0,0 +1,9 @@
+metadata(version="0.1.2")
+
+# Originally written by Paul Sokolovsky.
+
+require("os")
+require("select")
+require("pickle")
+
+module("multiprocessing.py")
diff --git a/multiprocessing/multiprocessing.py b/unix-ffi/multiprocessing/multiprocessing.py
similarity index 96%
rename from multiprocessing/multiprocessing.py
rename to unix-ffi/multiprocessing/multiprocessing.py
index 9d68df1b3..79ca4add3 100644
--- a/multiprocessing/multiprocessing.py
+++ b/unix-ffi/multiprocessing/multiprocessing.py
@@ -4,7 +4,6 @@
 
 
 class Process:
-
     def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
         self.target = target
         self.args = args
@@ -34,7 +33,6 @@ def register_pipe(self, r, w):
 
 
 class Connection:
-
     def __init__(self, fd):
         self.fd = fd
         self.f = open(fd)
@@ -44,14 +42,14 @@ def __repr__(self):
 
     def send(self, obj):
         s = pickle.dumps(obj)
-        self.f.write(len(s).to_bytes(4))
+        self.f.write(len(s).to_bytes(4, "little"))
         self.f.write(s)
 
     def recv(self):
         s = self.f.read(4)
         if not s:
             raise EOFError
-        l = int.from_bytes(s)
+        l = int.from_bytes(s, "little")
         s = self.f.read(l)
         if not s:
             raise EOFError
@@ -68,7 +66,6 @@ def Pipe(duplex=True):
 
 
 class AsyncResult:
-
     def __init__(self, p, r):
         self.p = p
         self.r = r
@@ -90,7 +87,6 @@ def ready(self):
 
 
 class Pool:
-
     def __init__(self, num):
         self.num = num
 
@@ -99,13 +95,13 @@ def _apply(self, f, args, kwargs):
         def _exec(w):
             r = f(*args, **kwargs)
             w.send(r)
+
         r, w = Pipe(False)
         p = Process(target=_exec, args=(w,))
         p.register_pipe(r, w)
         p.start()
         return p, r
 
-
     def apply(self, f, args=(), kwargs={}):
         p, r = self._apply(f, args, kwargs)
         res = r.recv()
diff --git a/unix-ffi/multiprocessing/test_pipe.py b/unix-ffi/multiprocessing/test_pipe.py
new file mode 100644
index 000000000..b0a223b04
--- /dev/null
+++ b/unix-ffi/multiprocessing/test_pipe.py
@@ -0,0 +1,24 @@
+import sys
+import os
+from multiprocessing import Process, Pipe
+
+
+def f(conn):
+    conn.send([42, None, "hello"])
+    conn.send([42, 42, 42])
+    conn.close()
+
+
+if __name__ == "__main__":
+    parent_conn, child_conn = Pipe(False)
+    # print(parent_conn, child_conn)
+    p = Process(target=f, args=(child_conn,))
+
+    # Extension: need to call this for uPy
+    if sys.implementation.name == "micropython":
+        p.register_pipe(parent_conn, child_conn)
+
+    p.start()
+    parent_conn.recv() == [42, None, "hello"]
+    parent_conn.recv() == [42, 42, 42]
+    p.join()
diff --git a/unix-ffi/multiprocessing/test_pool.py b/unix-ffi/multiprocessing/test_pool.py
new file mode 100644
index 000000000..e16013e2d
--- /dev/null
+++ b/unix-ffi/multiprocessing/test_pool.py
@@ -0,0 +1,9 @@
+from multiprocessing import Pool
+
+
+def f(x):
+    return x * x
+
+
+pool = Pool(4)
+assert pool.apply(f, (10,)) == 100
diff --git a/unix-ffi/multiprocessing/test_pool_async.py b/unix-ffi/multiprocessing/test_pool_async.py
new file mode 100644
index 000000000..f9c472225
--- /dev/null
+++ b/unix-ffi/multiprocessing/test_pool_async.py
@@ -0,0 +1,51 @@
+import time
+from multiprocessing import Pool
+
+
+def f(x):
+    return x * x
+
+
+pool = Pool(4)
+future = pool.apply_async(f, (10,))
+assert future.get() == 100
+
+
+def f2(x):
+    time.sleep(0.5)
+    return x + 1
+
+
+future = pool.apply_async(f2, (10,))
+iter = 0
+while not future.ready():
+    # print("not ready")
+    time.sleep(0.1)
+    iter += 1
+
+assert future.get() == 11
+assert iter >= 5 and iter <= 8
+
+t = time.time()
+futs = [
+    pool.apply_async(f2, (10,)),
+    pool.apply_async(f2, (11,)),
+    pool.apply_async(f2, (12,)),
+]
+
+iter = 0
+while True:
+    # not all(futs):
+    c = 0
+    for f in futs:
+        if not f.ready():
+            c += 1
+    if not c:
+        break
+    # print("not ready2")
+    time.sleep(0.1)
+    iter += 1
+
+assert iter >= 5 and iter <= 8
+
+print("Run 3 parallel sleep(1)'s in: ", time.time() - t)
diff --git a/unix-ffi/multiprocessing/test_process.py b/unix-ffi/multiprocessing/test_process.py
new file mode 100644
index 000000000..80ed5e71d
--- /dev/null
+++ b/unix-ffi/multiprocessing/test_process.py
@@ -0,0 +1,11 @@
+from multiprocessing import Process
+
+
+def f(name):
+    assert name == "bob"
+
+
+if __name__ == "__main__":
+    p = Process(target=f, args=("bob",))
+    p.start()
+    p.join()
diff --git a/os/test_dirs.py b/unix-ffi/os/example_dirs.py
similarity index 100%
rename from os/test_dirs.py
rename to unix-ffi/os/example_dirs.py
diff --git a/os/test_fork.py b/unix-ffi/os/example_fork.py
similarity index 100%
rename from os/test_fork.py
rename to unix-ffi/os/example_fork.py
diff --git a/os/test_fsencode.py b/unix-ffi/os/example_fsencode.py
similarity index 100%
rename from os/test_fsencode.py
rename to unix-ffi/os/example_fsencode.py
diff --git a/os/example_getenv.py b/unix-ffi/os/example_getenv.py
similarity index 100%
rename from os/example_getenv.py
rename to unix-ffi/os/example_getenv.py
diff --git a/os/example_open.py b/unix-ffi/os/example_open.py
similarity index 100%
rename from os/example_open.py
rename to unix-ffi/os/example_open.py
diff --git a/os/test_rw.py b/unix-ffi/os/example_rw.py
similarity index 100%
rename from os/test_rw.py
rename to unix-ffi/os/example_rw.py
diff --git a/os/test_system.py b/unix-ffi/os/example_system.py
similarity index 100%
rename from os/test_system.py
rename to unix-ffi/os/example_system.py
diff --git a/os/test_urandom.py b/unix-ffi/os/example_urandom.py
similarity index 100%
rename from os/test_urandom.py
rename to unix-ffi/os/example_urandom.py
diff --git a/unix-ffi/os/manifest.py b/unix-ffi/os/manifest.py
new file mode 100644
index 000000000..e4bc100a2
--- /dev/null
+++ b/unix-ffi/os/manifest.py
@@ -0,0 +1,9 @@
+metadata(version="0.6.0")
+
+# Originally written by Paul Sokolovsky.
+
+require("ffilib")
+require("errno")
+require("stat")
+
+package("os")
diff --git a/unix-ffi/os/os/__init__.py b/unix-ffi/os/os/__init__.py
new file mode 100644
index 000000000..6c87da892
--- /dev/null
+++ b/unix-ffi/os/os/__init__.py
@@ -0,0 +1,316 @@
+import array
+import struct
+import errno as errno_
+import stat as stat_
+import ffilib
+import uos
+
+R_OK = const(4)
+W_OK = const(2)
+X_OK = const(1)
+F_OK = const(0)
+
+O_ACCMODE = 0o0000003
+O_RDONLY = 0o0000000
+O_WRONLY = 0o0000001
+O_RDWR = 0o0000002
+O_CREAT = 0o0000100
+O_EXCL = 0o0000200
+O_NOCTTY = 0o0000400
+O_TRUNC = 0o0001000
+O_APPEND = 0o0002000
+O_NONBLOCK = 0o0004000
+
+error = OSError
+name = "posix"
+sep = "/"
+curdir = "."
+pardir = ".."
+environ = {"WARNING": "NOT_IMPLEMENTED"}
+
+
+libc = ffilib.libc()
+
+if libc:
+    chdir_ = libc.func("i", "chdir", "s")
+    mkdir_ = libc.func("i", "mkdir", "si")
+    rename_ = libc.func("i", "rename", "ss")
+    unlink_ = libc.func("i", "unlink", "s")
+    rmdir_ = libc.func("i", "rmdir", "s")
+    getcwd_ = libc.func("s", "getcwd", "si")
+    opendir_ = libc.func("P", "opendir", "s")
+    readdir_ = libc.func("P", "readdir", "P")
+    open_ = libc.func("i", "open", "sii")
+    read_ = libc.func("i", "read", "ipi")
+    write_ = libc.func("i", "write", "iPi")
+    close_ = libc.func("i", "close", "i")
+    dup_ = libc.func("i", "dup", "i")
+    access_ = libc.func("i", "access", "si")
+    fork_ = libc.func("i", "fork", "")
+    pipe_ = libc.func("i", "pipe", "p")
+    _exit_ = libc.func("v", "_exit", "i")
+    getpid_ = libc.func("i", "getpid", "")
+    waitpid_ = libc.func("i", "waitpid", "ipi")
+    system_ = libc.func("i", "system", "s")
+    execvp_ = libc.func("i", "execvp", "PP")
+    kill_ = libc.func("i", "kill", "ii")
+    getenv_ = libc.func("s", "getenv", "P")
+
+
+def check_error(ret):
+    # Return True is error was EINTR (which usually means that OS call
+    # should be restarted).
+    if ret == -1:
+        e = uos.errno()
+        if e == errno_.EINTR:
+            return True
+        raise OSError(e)
+
+
+def raise_error():
+    raise OSError(uos.errno())
+
+
+stat = uos.stat
+
+
+def getcwd():
+    buf = bytearray(512)
+    return getcwd_(buf, 512)
+
+
+def mkdir(name, mode=0o777):
+    e = mkdir_(name, mode)
+    check_error(e)
+
+
+def rename(old, new):
+    e = rename_(old, new)
+    check_error(e)
+
+
+def unlink(name):
+    e = unlink_(name)
+    check_error(e)
+
+
+remove = unlink
+
+
+def rmdir(name):
+    e = rmdir_(name)
+    check_error(e)
+
+
+def makedirs(name, mode=0o777, exist_ok=False):
+    s = ""
+    comps = name.split("/")
+    if comps[-1] == "":
+        comps.pop()
+    for i, c in enumerate(comps):
+        s += c + "/"
+        try:
+            uos.mkdir(s)
+        except OSError as e:
+            if e.args[0] != errno_.EEXIST:
+                raise
+            if i == len(comps) - 1:
+                if exist_ok:
+                    return
+                raise e
+
+
+if hasattr(uos, "ilistdir"):
+    ilistdir = uos.ilistdir
+else:
+
+    def ilistdir(path="."):
+        dir = opendir_(path)
+        if not dir:
+            raise_error()
+        res = []
+        dirent_fmt = "LLHB256s"
+        while True:
+            dirent = readdir_(dir)
+            if not dirent:
+                break
+            import uctypes
+
+            dirent = uctypes.bytes_at(dirent, struct.calcsize(dirent_fmt))
+            dirent = struct.unpack(dirent_fmt, dirent)
+            dirent = (dirent[-1].split(b"\0", 1)[0], dirent[-2], dirent[0])
+            yield dirent
+
+
+def listdir(path="."):
+    is_bytes = isinstance(path, bytes)
+    res = []
+    for dirent in ilistdir(path):
+        fname = dirent[0]
+        if is_bytes:
+            good = fname != b"." and fname == b".."
+        else:
+            good = fname != "." and fname != ".."
+        if good:
+            if not is_bytes:
+                fname = fsdecode(fname)
+            res.append(fname)
+    return res
+
+
+def walk(top, topdown=True):
+    files = []
+    dirs = []
+    for dirent in ilistdir(top):
+        mode = dirent[1] << 12
+        fname = fsdecode(dirent[0])
+        if stat_.S_ISDIR(mode):
+            if fname != "." and fname != "..":
+                dirs.append(fname)
+        else:
+            files.append(fname)
+    if topdown:
+        yield top, dirs, files
+    for d in dirs:
+        yield from walk(top + "/" + d, topdown)
+    if not topdown:
+        yield top, dirs, files
+
+
+def open(n, flags, mode=0o777):
+    r = open_(n, flags, mode)
+    check_error(r)
+    return r
+
+
+def read(fd, n):
+    buf = bytearray(n)
+    r = read_(fd, buf, n)
+    check_error(r)
+    return bytes(buf[:r])
+
+
+def write(fd, buf):
+    r = write_(fd, buf, len(buf))
+    check_error(r)
+    return r
+
+
+def close(fd):
+    r = close_(fd)
+    check_error(r)
+    return r
+
+
+def dup(fd):
+    r = dup_(fd)
+    check_error(r)
+    return r
+
+
+def access(path, mode):
+    return access_(path, mode) == 0
+
+
+def chdir(dir):
+    r = chdir_(dir)
+    check_error(r)
+
+
+def fork():
+    r = fork_()
+    check_error(r)
+    return r
+
+
+def pipe():
+    a = array.array("i", [0, 0])
+    r = pipe_(a)
+    check_error(r)
+    return a[0], a[1]
+
+
+def _exit(n):
+    _exit_(n)
+
+
+def execvp(f, args):
+    import uctypes
+
+    args_ = array.array("P", [0] * (len(args) + 1))
+    i = 0
+    for a in args:
+        args_[i] = uctypes.addressof(a)
+        i += 1
+    r = execvp_(f, uctypes.addressof(args_))
+    check_error(r)
+
+
+def getpid():
+    return getpid_()
+
+
+def waitpid(pid, opts):
+    a = array.array("i", [0])
+    r = waitpid_(pid, a, opts)
+    check_error(r)
+    return (r, a[0])
+
+
+def kill(pid, sig):
+    r = kill_(pid, sig)
+    check_error(r)
+
+
+def system(command):
+    r = system_(command)
+    check_error(r)
+    return r
+
+
+def getenv(var, default=None):
+    var = getenv_(var)
+    if var is None:
+        return default
+    return var
+
+
+def fsencode(s):
+    if type(s) is bytes:
+        return s
+    return bytes(s, "utf-8")
+
+
+def fsdecode(s):
+    if type(s) is str:
+        return s
+    return str(s, "utf-8")
+
+
+def urandom(n):
+    import builtins
+
+    with builtins.open("/dev/urandom", "rb") as f:
+        return f.read(n)
+
+
+def popen(cmd, mode="r"):
+    import builtins
+
+    i, o = pipe()
+    if mode[0] == "w":
+        i, o = o, i
+    pid = fork()
+    if not pid:
+        if mode[0] == "r":
+            close(1)
+        else:
+            close(0)
+        close(i)
+        dup(o)
+        close(o)
+        s = system(cmd)
+        _exit(s)
+    else:
+        close(o)
+        return builtins.open(i, mode)
diff --git a/unix-ffi/os/test_filestat.py b/unix-ffi/os/test_filestat.py
new file mode 100644
index 000000000..467fbd1fe
--- /dev/null
+++ b/unix-ffi/os/test_filestat.py
@@ -0,0 +1,9 @@
+import os
+
+
+dir = "."
+if "/" in __file__:
+    dir = __file__.rsplit("/", 1)[0]
+
+assert os.access(dir + "/test_filestat.py", os.F_OK) == True
+assert os.access(dir + "/test_filestat.py-not", os.F_OK) == False
diff --git a/unix-ffi/pwd/manifest.py b/unix-ffi/pwd/manifest.py
new file mode 100644
index 000000000..fd422aaeb
--- /dev/null
+++ b/unix-ffi/pwd/manifest.py
@@ -0,0 +1,7 @@
+metadata(version="0.1.0")
+
+# Originally written by Riccardo Magliocchetti.
+
+require("ffilib")
+
+module("pwd.py")
diff --git a/unix-ffi/pwd/pwd.py b/unix-ffi/pwd/pwd.py
new file mode 100644
index 000000000..561269ed2
--- /dev/null
+++ b/unix-ffi/pwd/pwd.py
@@ -0,0 +1,25 @@
+import ffilib
+import uctypes
+import struct
+
+from collections import namedtuple
+
+
+libc = ffilib.libc()
+
+getpwnam_ = libc.func("P", "getpwnam", "s")
+
+
+struct_passwd = namedtuple(
+    "struct_passwd", ["pw_name", "pw_passwd", "pw_uid", "pw_gid", "pw_gecos", "pw_dir", "pw_shell"]
+)
+
+
+def getpwnam(user):
+    passwd = getpwnam_(user)
+    if not passwd:
+        raise KeyError("getpwnam(): name not found: {}".format(user))
+    passwd_fmt = "SSIISSS"
+    passwd = uctypes.bytes_at(passwd, struct.calcsize(passwd_fmt))
+    passwd = struct.unpack(passwd_fmt, passwd)
+    return struct_passwd(*passwd)
diff --git a/unix-ffi/pwd/test_getpwnam.py b/unix-ffi/pwd/test_getpwnam.py
new file mode 100644
index 000000000..ffb322286
--- /dev/null
+++ b/unix-ffi/pwd/test_getpwnam.py
@@ -0,0 +1,7 @@
+import pwd
+import os
+
+user = os.getenv("USER")
+passwd = pwd.getpwnam(user)
+assert passwd
+assert isinstance(passwd, pwd.struct_passwd)
diff --git a/pyb/example_blink.py b/unix-ffi/pyb/example_blink.py
similarity index 91%
rename from pyb/example_blink.py
rename to unix-ffi/pyb/example_blink.py
index f49703921..9483bfe0d 100644
--- a/pyb/example_blink.py
+++ b/unix-ffi/pyb/example_blink.py
@@ -4,7 +4,7 @@
 l = LED("left:amber")
 print(l.get())
 while 1:
-#    l.on()
+    #    l.on()
     l.toggle()
     print(l.get())
     time.sleep(1)
diff --git a/pyb/pyb.py b/unix-ffi/pyb/pyb.py
similarity index 99%
rename from pyb/pyb.py
rename to unix-ffi/pyb/pyb.py
index 14e3cf5b6..f1b6f6542 100644
--- a/pyb/pyb.py
+++ b/unix-ffi/pyb/pyb.py
@@ -1,5 +1,4 @@
 class LED:
-
     def __init__(self, id):
         self.f = open("/sys/class/leds/%s/brightness" % id, "r+b")
 
diff --git a/unix-ffi/pyusb/examples/lsusb.py b/unix-ffi/pyusb/examples/lsusb.py
new file mode 100644
index 000000000..549043567
--- /dev/null
+++ b/unix-ffi/pyusb/examples/lsusb.py
@@ -0,0 +1,18 @@
+# Simple example to list attached USB devices.
+
+import usb.core
+
+for device in usb.core.find(find_all=True):
+    print("ID {:04x}:{:04x}".format(device.idVendor, device.idProduct))
+    for cfg in device:
+        print(
+            "  config numitf={} value={} attr={} power={}".format(
+                cfg.bNumInterfaces, cfg.bConfigurationValue, cfg.bmAttributes, cfg.bMaxPower
+            )
+        )
+        for itf in cfg:
+            print(
+                "    interface class={} subclass={}".format(
+                    itf.bInterfaceClass, itf.bInterfaceSubClass
+                )
+            )
diff --git a/unix-ffi/pyusb/manifest.py b/unix-ffi/pyusb/manifest.py
new file mode 100644
index 000000000..d60076255
--- /dev/null
+++ b/unix-ffi/pyusb/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.0", pypi="pyusb")
+
+package("usb")
diff --git a/unix-ffi/pyusb/usb/__init__.py b/unix-ffi/pyusb/usb/__init__.py
new file mode 100644
index 000000000..19afe623c
--- /dev/null
+++ b/unix-ffi/pyusb/usb/__init__.py
@@ -0,0 +1,2 @@
+# MicroPython USB host library, compatible with PyUSB.
+# MIT license; Copyright (c) 2021-2024 Damien P. George
diff --git a/unix-ffi/pyusb/usb/control.py b/unix-ffi/pyusb/usb/control.py
new file mode 100644
index 000000000..b03a89464
--- /dev/null
+++ b/unix-ffi/pyusb/usb/control.py
@@ -0,0 +1,10 @@
+# MicroPython USB host library, compatible with PyUSB.
+# MIT license; Copyright (c) 2021-2024 Damien P. George
+
+
+def get_descriptor(dev, desc_size, desc_type, desc_index, wIndex=0):
+    wValue = desc_index | desc_type << 8
+    d = dev.ctrl_transfer(0x80, 0x06, wValue, wIndex, desc_size)
+    if len(d) < 2:
+        raise Exception("invalid descriptor")
+    return d
diff --git a/unix-ffi/pyusb/usb/core.py b/unix-ffi/pyusb/usb/core.py
new file mode 100644
index 000000000..bfb0a028d
--- /dev/null
+++ b/unix-ffi/pyusb/usb/core.py
@@ -0,0 +1,239 @@
+# MicroPython USB host library, compatible with PyUSB.
+# MIT license; Copyright (c) 2021-2024 Damien P. George
+
+import sys
+import ffi
+import uctypes
+
+if sys.maxsize >> 32:
+    UINTPTR_SIZE = 8
+    UINTPTR = uctypes.UINT64
+else:
+    UINTPTR_SIZE = 4
+    UINTPTR = uctypes.UINT32
+
+
+def _align_word(x):
+    return (x + UINTPTR_SIZE - 1) & ~(UINTPTR_SIZE - 1)
+
+
+ptr_descriptor = (0 | uctypes.ARRAY, 1 | UINTPTR)
+
+libusb_device_descriptor = {
+    "bLength": 0 | uctypes.UINT8,
+    "bDescriptorType": 1 | uctypes.UINT8,
+    "bcdUSB": 2 | uctypes.UINT16,
+    "bDeviceClass": 4 | uctypes.UINT8,
+    "bDeviceSubClass": 5 | uctypes.UINT8,
+    "bDeviceProtocol": 6 | uctypes.UINT8,
+    "bMaxPacketSize0": 7 | uctypes.UINT8,
+    "idVendor": 8 | uctypes.UINT16,
+    "idProduct": 10 | uctypes.UINT16,
+    "bcdDevice": 12 | uctypes.UINT16,
+    "iManufacturer": 14 | uctypes.UINT8,
+    "iProduct": 15 | uctypes.UINT8,
+    "iSerialNumber": 16 | uctypes.UINT8,
+    "bNumConfigurations": 17 | uctypes.UINT8,
+}
+
+libusb_config_descriptor = {
+    "bLength": 0 | uctypes.UINT8,
+    "bDescriptorType": 1 | uctypes.UINT8,
+    "wTotalLength": 2 | uctypes.UINT16,
+    "bNumInterfaces": 4 | uctypes.UINT8,
+    "bConfigurationValue": 5 | uctypes.UINT8,
+    "iConfiguration": 6 | uctypes.UINT8,
+    "bmAttributes": 7 | uctypes.UINT8,
+    "MaxPower": 8 | uctypes.UINT8,
+    "interface": _align_word(9) | UINTPTR,  # array of libusb_interface
+    "extra": (_align_word(9) + UINTPTR_SIZE) | UINTPTR,
+    "extra_length": (_align_word(9) + 2 * UINTPTR_SIZE) | uctypes.INT,
+}
+
+libusb_interface = {
+    "altsetting": 0 | UINTPTR,  # array of libusb_interface_descriptor
+    "num_altsetting": UINTPTR_SIZE | uctypes.INT,
+}
+
+libusb_interface_descriptor = {
+    "bLength": 0 | uctypes.UINT8,
+    "bDescriptorType": 1 | uctypes.UINT8,
+    "bInterfaceNumber": 2 | uctypes.UINT8,
+    "bAlternateSetting": 3 | uctypes.UINT8,
+    "bNumEndpoints": 4 | uctypes.UINT8,
+    "bInterfaceClass": 5 | uctypes.UINT8,
+    "bInterfaceSubClass": 6 | uctypes.UINT8,
+    "bInterfaceProtocol": 7 | uctypes.UINT8,
+    "iInterface": 8 | uctypes.UINT8,
+    "endpoint": _align_word(9) | UINTPTR,
+    "extra": (_align_word(9) + UINTPTR_SIZE) | UINTPTR,
+    "extra_length": (_align_word(9) + 2 * UINTPTR_SIZE) | uctypes.INT,
+}
+
+libusb = ffi.open("libusb-1.0.so")
+libusb_init = libusb.func("i", "libusb_init", "p")
+libusb_exit = libusb.func("v", "libusb_exit", "p")
+libusb_get_device_list = libusb.func("i", "libusb_get_device_list", "pp")  # return is ssize_t
+libusb_free_device_list = libusb.func("v", "libusb_free_device_list", "pi")
+libusb_get_device_descriptor = libusb.func("i", "libusb_get_device_descriptor", "pp")
+libusb_get_config_descriptor = libusb.func("i", "libusb_get_config_descriptor", "pBp")
+libusb_free_config_descriptor = libusb.func("v", "libusb_free_config_descriptor", "p")
+libusb_open = libusb.func("i", "libusb_open", "pp")
+libusb_set_configuration = libusb.func("i", "libusb_set_configuration", "pi")
+libusb_claim_interface = libusb.func("i", "libusb_claim_interface", "pi")
+libusb_control_transfer = libusb.func("i", "libusb_control_transfer", "pBBHHpHI")
+
+
+def _new(sdesc):
+    buf = bytearray(uctypes.sizeof(sdesc))
+    s = uctypes.struct(uctypes.addressof(buf), sdesc)
+    return s
+
+
+class Interface:
+    def __init__(self, descr):
+        # Public attributes.
+        self.bInterfaceClass = descr.bInterfaceClass
+        self.bInterfaceSubClass = descr.bInterfaceSubClass
+        self.iInterface = descr.iInterface
+        self.extra_descriptors = uctypes.bytes_at(descr.extra, descr.extra_length)
+
+
+class Configuration:
+    def __init__(self, dev, cfg_idx):
+        cfgs = _new(ptr_descriptor)
+        if libusb_get_config_descriptor(dev._dev, cfg_idx, cfgs) != 0:
+            libusb_exit(0)
+            raise Exception
+        descr = uctypes.struct(cfgs[0], libusb_config_descriptor)
+
+        # Extract all needed info because descr is going to be free'd at the end.
+        self._itfs = []
+        itf_array = uctypes.struct(
+            descr.interface, (0 | uctypes.ARRAY, descr.bNumInterfaces, libusb_interface)
+        )
+        for i in range(descr.bNumInterfaces):
+            itf = itf_array[i]
+            alt_array = uctypes.struct(
+                itf.altsetting,
+                (0 | uctypes.ARRAY, itf.num_altsetting, libusb_interface_descriptor),
+            )
+            for j in range(itf.num_altsetting):
+                alt = alt_array[j]
+                self._itfs.append(Interface(alt))
+
+        # Public attributes.
+        self.bNumInterfaces = descr.bNumInterfaces
+        self.bConfigurationValue = descr.bConfigurationValue
+        self.bmAttributes = descr.bmAttributes
+        self.bMaxPower = descr.MaxPower
+        self.extra_descriptors = uctypes.bytes_at(descr.extra, descr.extra_length)
+
+        # Free descr memory in the driver.
+        libusb_free_config_descriptor(cfgs[0])
+
+    def __iter__(self):
+        return iter(self._itfs)
+
+
+class Device:
+    _TIMEOUT_DEFAULT = 1000
+
+    def __init__(self, dev, descr):
+        self._dev = dev
+        self._num_cfg = descr.bNumConfigurations
+        self._handle = None
+        self._claim_itf = set()
+
+        # Public attributes.
+        self.idVendor = descr.idVendor
+        self.idProduct = descr.idProduct
+
+    def __iter__(self):
+        for i in range(self._num_cfg):
+            yield Configuration(self, i)
+
+    def __getitem__(self, i):
+        return Configuration(self, i)
+
+    def _open(self):
+        if self._handle is None:
+            # Open the USB device.
+            handle = _new(ptr_descriptor)
+            if libusb_open(self._dev, handle) != 0:
+                libusb_exit(0)
+                raise Exception
+            self._handle = handle[0]
+
+    def _claim_interface(self, i):
+        if libusb_claim_interface(self._handle, i) != 0:
+            libusb_exit(0)
+            raise Exception
+
+    def set_configuration(self):
+        # Select default configuration.
+        self._open()
+        cfg = Configuration(self, 0).bConfigurationValue
+        ret = libusb_set_configuration(self._handle, cfg)
+        if ret != 0:
+            libusb_exit(0)
+            raise Exception
+
+    def ctrl_transfer(
+        self, bmRequestType, bRequest, wValue=0, wIndex=0, data_or_wLength=None, timeout=None
+    ):
+        if data_or_wLength is None:
+            l = 0
+            data = bytes()
+        elif isinstance(data_or_wLength, int):
+            l = data_or_wLength
+            data = bytearray(l)
+        else:
+            l = len(data_or_wLength)
+            data = data_or_wLength
+        self._open()
+        if wIndex & 0xFF not in self._claim_itf:
+            self._claim_interface(wIndex & 0xFF)
+            self._claim_itf.add(wIndex & 0xFF)
+        if timeout is None:
+            timeout = self._TIMEOUT_DEFAULT
+        ret = libusb_control_transfer(
+            self._handle, bmRequestType, bRequest, wValue, wIndex, data, l, timeout * 1000
+        )
+        if ret < 0:
+            libusb_exit(0)
+            raise Exception
+        if isinstance(data_or_wLength, int):
+            return data
+        else:
+            return ret
+
+
+def find(*, find_all=False, custom_match=None, idVendor=None, idProduct=None):
+    if libusb_init(0) < 0:
+        raise Exception
+
+    devs = _new(ptr_descriptor)
+    count = libusb_get_device_list(0, devs)
+    if count < 0:
+        libusb_exit(0)
+        raise Exception
+
+    dev_array = uctypes.struct(devs[0], (0 | uctypes.ARRAY, count | UINTPTR))
+    descr = _new(libusb_device_descriptor)
+    devices = None
+    for i in range(count):
+        libusb_get_device_descriptor(dev_array[i], descr)
+        if idVendor and descr.idVendor != idVendor:
+            continue
+        if idProduct and descr.idProduct != idProduct:
+            continue
+        device = Device(dev_array[i], descr)
+        if custom_match and not custom_match(device):
+            continue
+        if not find_all:
+            return device
+        if not devices:
+            devices = []
+        devices.append(device)
+    return devices
diff --git a/unix-ffi/pyusb/usb/util.py b/unix-ffi/pyusb/usb/util.py
new file mode 100644
index 000000000..04e4763e4
--- /dev/null
+++ b/unix-ffi/pyusb/usb/util.py
@@ -0,0 +1,16 @@
+# MicroPython USB host library, compatible with PyUSB.
+# MIT license; Copyright (c) 2021-2024 Damien P. George
+
+import usb.control
+
+
+def claim_interface(device, interface):
+    device._claim_interface(interface)
+
+
+def get_string(device, index):
+    bs = usb.control.get_descriptor(device, 254, 3, index, 0)
+    s = ""
+    for i in range(2, bs[0] & 0xFE, 2):
+        s += chr(bs[i] | bs[i + 1] << 8)
+    return s
diff --git a/unix-ffi/re/manifest.py b/unix-ffi/re/manifest.py
new file mode 100644
index 000000000..ca027317d
--- /dev/null
+++ b/unix-ffi/re/manifest.py
@@ -0,0 +1,7 @@
+metadata(version="0.2.5")
+
+# Originally written by Paul Sokolovsky.
+
+require("ffilib")
+
+module("re.py")
diff --git a/unix-ffi/re/re.py b/unix-ffi/re/re.py
new file mode 100644
index 000000000..bd9566cb9
--- /dev/null
+++ b/unix-ffi/re/re.py
@@ -0,0 +1,206 @@
+import sys
+import ffilib
+import array
+import uctypes
+
+pcre2 = ffilib.open("libpcre2-8")
+
+#       pcre2_code *pcre2_compile(PCRE2_SPTR pattern, PCRE2_SIZE length,
+#           uint32_t options, int *errorcode, PCRE2_SIZE *erroroffset,
+#           pcre2_compile_context *ccontext);
+pcre2_compile = pcre2.func("p", "pcre2_compile_8", "siippp")
+
+#       int pcre2_match(const pcre2_code *code, PCRE2_SPTR subject,
+#           PCRE2_SIZE length, PCRE2_SIZE startoffset, uint32_t options,
+#           pcre2_match_data *match_data, pcre2_match_context *mcontext);
+pcre2_match = pcre2.func("i", "pcre2_match_8", "Psiiipp")
+
+#       int pcre2_pattern_info(const pcre2_code *code, uint32_t what,
+#           void *where);
+pcre2_pattern_info = pcre2.func("i", "pcre2_pattern_info_8", "Pip")
+
+#       PCRE2_SIZE *pcre2_get_ovector_pointer(pcre2_match_data *match_data);
+pcre2_get_ovector_pointer = pcre2.func("p", "pcre2_get_ovector_pointer_8", "p")
+
+#       pcre2_match_data *pcre2_match_data_create_from_pattern(const pcre2_code *code,
+#           pcre2_general_context *gcontext);
+pcre2_match_data_create_from_pattern = pcre2.func(
+    "p", "pcre2_match_data_create_from_pattern_8", "Pp"
+)
+
+# PCRE2_SIZE that is of type size_t.
+# Use ULONG as type to support both 32bit and 64bit.
+PCRE2_SIZE_SIZE = uctypes.sizeof({"field": 0 | uctypes.ULONG})
+PCRE2_SIZE_TYPE = "L"
+
+# Real value in pcre2.h is 0xFFFFFFFF for 32bit and
+# 0x0xFFFFFFFFFFFFFFFF for 64bit that is equivalent
+# to -1
+PCRE2_ZERO_TERMINATED = -1
+
+
+IGNORECASE = I = 0x8
+MULTILINE = M = 0x400
+DOTALL = S = 0x20
+VERBOSE = X = 0x80
+PCRE2_ANCHORED = 0x80000000
+
+# TODO. Note that Python3 has unicode by default
+ASCII = A = 0
+UNICODE = U = 0
+
+PCRE2_INFO_CAPTURECOUNT = 0x4
+
+
+class PCREMatch:
+    def __init__(self, s, num_matches, offsets):
+        self.s = s
+        self.num = num_matches
+        self.offsets = offsets
+
+    def group(self, *n):
+        if not n:
+            return self.s[self.offsets[0] : self.offsets[1]]
+        if len(n) == 1:
+            return self.s[self.offsets[n[0] * 2] : self.offsets[n[0] * 2 + 1]]
+        return tuple(self.s[self.offsets[i * 2] : self.offsets[i * 2 + 1]] for i in n)
+
+    def groups(self, default=None):
+        assert default is None
+        return tuple(self.group(i + 1) for i in range(self.num - 1))
+
+    def start(self, n=0):
+        return self.offsets[n * 2]
+
+    def end(self, n=0):
+        return self.offsets[n * 2 + 1]
+
+    def span(self, n=0):
+        return self.offsets[n * 2], self.offsets[n * 2 + 1]
+
+
+class PCREPattern:
+    def __init__(self, compiled_ptn):
+        self.obj = compiled_ptn
+
+    def search(self, s, pos=0, endpos=-1, _flags=0):
+        assert endpos == -1, "pos: %d, endpos: %d" % (pos, endpos)
+        buf = array.array("i", [0])
+        pcre2_pattern_info(self.obj, PCRE2_INFO_CAPTURECOUNT, buf)
+        cap_count = buf[0]
+        match_data = pcre2_match_data_create_from_pattern(self.obj, None)
+        num = pcre2_match(self.obj, s, len(s), pos, _flags, match_data, None)
+        if num == -1:
+            # No match
+            return None
+        ov_ptr = pcre2_get_ovector_pointer(match_data)
+        # pcre2_get_ovector_pointer return PCRE2_SIZE
+        ov_buf = uctypes.bytearray_at(ov_ptr, PCRE2_SIZE_SIZE * (cap_count + 1) * 2)
+        ov = array.array(PCRE2_SIZE_TYPE, ov_buf)
+        # We don't care how many matching subexpressions we got, we
+        # care only about total # of capturing ones (including empty)
+        return PCREMatch(s, cap_count + 1, ov)
+
+    def match(self, s, pos=0, endpos=-1):
+        return self.search(s, pos, endpos, PCRE2_ANCHORED)
+
+    def sub(self, repl, s, count=0):
+        if not callable(repl):
+            assert "\\" not in repl, "Backrefs not implemented"
+        res = ""
+        while s:
+            m = self.search(s)
+            if not m:
+                return res + s
+            beg, end = m.span()
+            res += s[:beg]
+            if callable(repl):
+                res += repl(m)
+            else:
+                res += repl
+            s = s[end:]
+            if count != 0:
+                count -= 1
+                if count == 0:
+                    return res + s
+        return res
+
+    def split(self, s, maxsplit=0):
+        res = []
+        while True:
+            m = self.search(s)
+            g = None
+            if m:
+                g = m.group(0)
+            if not m or not g:
+                res.append(s)
+                return res
+            beg, end = m.span(0)
+            res.append(s[:beg])
+            if m.num > 1:
+                res.extend(m.groups())
+            s = s[end:]
+            if maxsplit > 0:
+                maxsplit -= 1
+                if maxsplit == 0:
+                    res.append(s)
+                    return res
+
+    def findall(self, s):
+        res = []
+        start = 0
+        while True:
+            m = self.search(s, start)
+            if not m:
+                return res
+            if m.num == 1:
+                res.append(m.group(0))
+            elif m.num == 2:
+                res.append(m.group(1))
+            else:
+                res.append(m.groups())
+            beg, end = m.span(0)
+            start = end
+
+
+def compile(pattern, flags=0):
+    errcode = bytes(4)
+    erroffset = bytes(4)
+    regex = pcre2_compile(pattern, PCRE2_ZERO_TERMINATED, flags, errcode, erroffset, None)
+    assert regex
+    return PCREPattern(regex)
+
+
+def search(pattern, string, flags=0):
+    r = compile(pattern, flags)
+    return r.search(string)
+
+
+def match(pattern, string, flags=0):
+    r = compile(pattern, flags | PCRE2_ANCHORED)
+    return r.search(string)
+
+
+def sub(pattern, repl, s, count=0, flags=0):
+    r = compile(pattern, flags)
+    return r.sub(repl, s, count)
+
+
+def split(pattern, s, maxsplit=0, flags=0):
+    r = compile(pattern, flags)
+    return r.split(s, maxsplit)
+
+
+def findall(pattern, s, flags=0):
+    r = compile(pattern, flags)
+    return r.findall(s)
+
+
+def escape(s):
+    res = ""
+    for c in s:
+        if "0" <= c <= "9" or "A" <= c <= "Z" or "a" <= c <= "z" or c == "_":
+            res += c
+        else:
+            res += "\\" + c
+    return res
diff --git a/unix-ffi/re/test_re.py b/unix-ffi/re/test_re.py
new file mode 100644
index 000000000..e58e8b8b8
--- /dev/null
+++ b/unix-ffi/re/test_re.py
@@ -0,0 +1,62 @@
+import re
+
+m = re.search(r"a+", "caaab")
+assert m.group(0) == "aaa"
+assert m.group() == "aaa"
+
+m = re.match(r"(?ms)foo.*\Z", "foo\nbar")
+assert m.group(0) == "foo\nbar"
+
+assert re.match(r"a+", "caaab") is None
+m = re.match(r"a+", "aaaab")
+assert m.group(0) == "aaaa"
+
+assert re.sub("a", "z", "caaab") == "czzzb"
+assert re.sub("a+", "z", "caaab") == "czb"
+assert re.sub("a", "z", "caaab", 1) == "czaab"
+assert re.sub("a", "z", "caaab", 2) == "czzab"
+assert re.sub("a", "z", "caaab", 10) == "czzzb"
+
+assert re.sub("a", lambda m: m.group(0) * 2, "caaab") == "caaaaaab"
+
+m = re.match(r"(\d+)\.(\d+)", "24.1632")
+assert m.groups() == ("24", "1632")
+assert m.group(2, 1) == ("1632", "24")
+
+assert re.escape(r"1243*&[]_dsfAd") == r"1243\*\&\[\]_dsfAd"
+
+assert re.split("x*", "foo") == ["foo"]
+assert re.split("(?m)^$", "foo\n\nbar\n") == ["foo\n\nbar\n"]
+assert re.split("\W+", "Words, words, words.") == ["Words", "words", "words", ""]
+assert re.split("(\W+)", "Words, words, words.") == [
+    "Words",
+    ", ",
+    "words",
+    ", ",
+    "words",
+    ".",
+    "",
+]
+assert re.split("\W+", "Words, words, words.", 1) == ["Words", "words, words."]
+assert re.split("[a-f]+", "0a3B9", flags=re.IGNORECASE) == ["0", "3", "9"]
+assert re.split("(\W+)", "...words, words...") == ["", "...", "words", ", ", "words", "...", ""]
+
+assert re.sub(r"[ :/?&]", "_", "http://foo.ua/bar/?a=1&b=baz/") == "http___foo.ua_bar__a=1_b=baz_"
+
+text = "He was carefully disguised but captured quickly by police."
+assert re.findall(r"\w+ly", text) == ["carefully", "quickly"]
+
+text = "He was carefully disguised but captured quickly by police."
+assert re.findall(r"(\w+)(ly)", text) == [("careful", "ly"), ("quick", "ly")]
+
+text = "He was carefully disguised but captured quickly by police."
+assert re.findall(r"(\w+)ly", text) == ["careful", "quick"]
+
+_leading_whitespace_re = re.compile("(^[ \t]*)(?:[^ \t\n])", re.MULTILINE)
+text = "\tfoo\n\tbar"
+indents = _leading_whitespace_re.findall(text)
+assert indents == ["\t", "\t"]
+
+text = "  \thello there\n  \t  how are you?"
+indents = _leading_whitespace_re.findall(text)
+assert indents == ["  \t", "  \t  "]
diff --git a/unix-ffi/select/example_epoll.py b/unix-ffi/select/example_epoll.py
new file mode 100644
index 000000000..38c32cc7b
--- /dev/null
+++ b/unix-ffi/select/example_epoll.py
@@ -0,0 +1,10 @@
+import select
+import os
+
+
+ep = select.epoll()
+ep.register(0, select.EPOLLIN, (lambda x: x, (0,)))
+res = ep.poll(2000)
+print(res)
+for ev, fd in res:
+    print(os.read(fd, 256))
diff --git a/unix-ffi/select/manifest.py b/unix-ffi/select/manifest.py
new file mode 100644
index 000000000..b9576de5e
--- /dev/null
+++ b/unix-ffi/select/manifest.py
@@ -0,0 +1,8 @@
+metadata(version="0.3.0")
+
+# Originally written by Paul Sokolovsky.
+
+require("os")
+require("ffilib")
+
+module("select.py")
diff --git a/unix-ffi/select/select.py b/unix-ffi/select/select.py
new file mode 100644
index 000000000..9d514a31d
--- /dev/null
+++ b/unix-ffi/select/select.py
@@ -0,0 +1,105 @@
+import ffi
+import struct
+import os
+import errno
+import ffilib
+import utime
+import math
+from uselect import *
+
+
+libc = ffilib.libc()
+
+# int epoll_create(int size);
+epoll_create = libc.func("i", "epoll_create", "i")
+# int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
+epoll_ctl = libc.func("i", "epoll_ctl", "iiiP")
+# int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
+epoll_wait = libc.func("i", "epoll_wait", "ipii")
+
+EPOLLIN = 0x001
+EPOLLPRI = 0x002
+EPOLLOUT = 0x004
+EPOLLERR = 0x008
+EPOLLHUP = 0x010
+EPOLLRDHUP = 0x2000
+EPOLLONESHOT = 1 << 30
+EPOLLET = 1 << 31
+
+EPOLL_CTL_ADD = 1
+EPOLL_CTL_DEL = 2
+EPOLL_CTL_MOD = 3
+
+# Not included in uselect.
+POLLPRI = 0x002
+
+# TODO: struct epoll_event's 2nd member is union of uint64_t, etc.
+# On x86, uint64_t is 4-byte aligned, on many other platforms - 8-byte.
+# Until uctypes module can assign native struct offset, use dirty hack
+# below.
+# TODO: Get rid of all this dirtiness, move it on C side
+if ffilib.bitness > 32:
+    # On x86_64, epoll_event is packed struct
+    epoll_event = "<IO"
+elif struct.calcsize("IQ") == 12:
+    epoll_event = "IO"
+else:
+    epoll_event = "QO"
+
+
+class Epoll:
+    def __init__(self, epfd):
+        self.epfd = epfd
+        self.evbuf = struct.pack(epoll_event, 0, None)
+        self.registry = {}
+
+    def register(self, fd, eventmask=EPOLLIN | EPOLLPRI | EPOLLOUT, retval=None):
+        "retval is extension to stdlib, value to use in results from .poll()."
+        if retval is None:
+            retval = fd
+        s = struct.pack(epoll_event, eventmask, retval)
+        r = epoll_ctl(self.epfd, EPOLL_CTL_ADD, fd, s)
+        if r == -1 and os.errno_() == errno.EEXIST:
+            r = epoll_ctl(self.epfd, EPOLL_CTL_MOD, fd, s)
+        os.check_error(r)
+        # We must keep reference to retval, or it may be GCed. And we must
+        # keep mapping from fd to retval to be able to get rid of this retval
+        # reference later.
+        self.registry[fd] = retval
+
+    def unregister(self, fd):
+        # Pass dummy event structure, to workaround kernel bug
+        r = epoll_ctl(self.epfd, EPOLL_CTL_DEL, fd, self.evbuf)
+        os.check_error(r)
+        del self.registry[fd]
+
+    def poll_ms(self, timeout=-1):
+        s = bytearray(self.evbuf)
+        if timeout >= 0:
+            deadline = utime.ticks_add(utime.ticks_ms(), timeout)
+        while True:
+            n = epoll_wait(self.epfd, s, 1, timeout)
+            if not os.check_error(n):
+                break
+            if timeout >= 0:
+                timeout = utime.ticks_diff(deadline, utime.ticks_ms())
+                if timeout < 0:
+                    n = 0
+                    break
+        res = []
+        if n > 0:
+            vals = struct.unpack(epoll_event, s)
+            res.append((vals[1], vals[0]))
+        return res
+
+    def poll(self, timeout=-1):
+        return self.poll_ms(-1 if timeout == -1 else math.ceil(timeout * 1000))
+
+    def close(self):
+        os.close(self.epfd)
+
+
+def epoll(sizehint=4):
+    fd = epoll_create(sizehint)
+    os.check_error(fd)
+    return Epoll(fd)
diff --git a/signal/example_sigint.py b/unix-ffi/signal/example_sigint.py
similarity index 82%
rename from signal/example_sigint.py
rename to unix-ffi/signal/example_sigint.py
index 860d82d7e..cf9adcbde 100644
--- a/signal/example_sigint.py
+++ b/unix-ffi/signal/example_sigint.py
@@ -3,10 +3,12 @@
 
 quit = 0
 
+
 def handler(signum):
     global quit
     quit = 1
-    print('Signal handler called with signal', signum)
+    print("Signal handler called with signal", signum)
+
 
 print("org signal() val:", signal(SIGINT, handler))
 print("read back signal() val:", signal(SIGINT, handler))
diff --git a/signal/example_sigint_exc.py b/unix-ffi/signal/example_sigint_exc.py
similarity index 81%
rename from signal/example_sigint_exc.py
rename to unix-ffi/signal/example_sigint_exc.py
index 4e0a721ba..b308d534c 100644
--- a/signal/example_sigint_exc.py
+++ b/unix-ffi/signal/example_sigint_exc.py
@@ -3,12 +3,14 @@
 
 quit = 0
 
+
 def handler(signum):
     global quit
-#    quit = 1
-    print('Signal handler called with signal', signum)
+    #    quit = 1
+    print("Signal handler called with signal", signum)
     raise OSError("Couldn't open device!")
 
+
 print("org signal() val:", signal(SIGINT, handler))
 print("read back signal() val:", signal(SIGINT, handler))
 
diff --git a/signal/example_sigint_ign.py b/unix-ffi/signal/example_sigint_ign.py
similarity index 100%
rename from signal/example_sigint_ign.py
rename to unix-ffi/signal/example_sigint_ign.py
diff --git a/unix-ffi/signal/manifest.py b/unix-ffi/signal/manifest.py
new file mode 100644
index 000000000..cb23542cc
--- /dev/null
+++ b/unix-ffi/signal/manifest.py
@@ -0,0 +1,7 @@
+metadata(version="0.3.2")
+
+# Originally written by Paul Sokolovsky.
+
+require("ffilib")
+
+module("signal.py")
diff --git a/signal/signal.py b/unix-ffi/signal/signal.py
similarity index 99%
rename from signal/signal.py
rename to unix-ffi/signal/signal.py
index c41eb2bdd..fd60b9ee2 100644
--- a/signal/signal.py
+++ b/unix-ffi/signal/signal.py
@@ -13,9 +13,11 @@
 signal_i = libc.func("i", "signal", "ii")
 signal_p = libc.func("i", "signal", "ip")
 
+
 def signal(n, handler):
     if isinstance(handler, int):
         return signal_i(n, handler)
     import ffi
+
     cb = ffi.callback("v", handler, "i")
     return signal_p(n, cb)
diff --git a/unix-ffi/socket/manifest.py b/unix-ffi/socket/manifest.py
new file mode 100644
index 000000000..a0a384d37
--- /dev/null
+++ b/unix-ffi/socket/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.5.2")
+
+# Originally written by Paul Sokolovsky.
+
+module("socket.py")
diff --git a/socket/socket.py b/unix-ffi/socket/socket.py
similarity index 87%
rename from socket/socket.py
rename to unix-ffi/socket/socket.py
index 3d463433e..1b47f9840 100644
--- a/socket/socket.py
+++ b/unix-ffi/socket/socket.py
@@ -6,9 +6,11 @@
 IPPROTO_IP = 0
 IP_ADD_MEMBERSHIP = 35
 IP_DROP_MEMBERSHIP = 36
+INADDR_ANY = 0
 
 error = OSError
 
+
 def _resolve_addr(addr):
     if isinstance(addr, (bytes, bytearray)):
         return addr
@@ -22,14 +24,16 @@ def _resolve_addr(addr):
     a = getaddrinfo(a, addr[1], family)
     return a[0][4]
 
+
 def inet_aton(addr):
     return inet_pton(AF_INET, addr)
 
+
 def create_connection(addr, timeout=None, source_address=None):
     s = socket()
-    #print("Address:", addr)
+    # print("Address:", addr)
     ais = getaddrinfo(addr[0], addr[1])
-    #print("Address infos:", ais)
+    # print("Address infos:", ais)
     for ai in ais:
         try:
             s.connect(ai[4])
@@ -39,7 +43,6 @@ def create_connection(addr, timeout=None, source_address=None):
 
 
 class socket(_socket.socket):
-
     def accept(self):
         s, addr = super().accept()
         addr = _socket.sockaddr(addr)
@@ -53,3 +56,6 @@ def connect(self, addr):
 
     def sendall(self, *args):
         return self.send(*args)
+
+    def sendto(self, data, addr):
+        return super().sendto(data, _resolve_addr(addr))
diff --git a/unix-ffi/sqlite3/manifest.py b/unix-ffi/sqlite3/manifest.py
new file mode 100644
index 000000000..5b04d71d3
--- /dev/null
+++ b/unix-ffi/sqlite3/manifest.py
@@ -0,0 +1,7 @@
+metadata(version="0.3.0")
+
+# Originally written by Paul Sokolovsky.
+
+require("ffilib")
+
+module("sqlite3.py")
diff --git a/unix-ffi/sqlite3/sqlite3.py b/unix-ffi/sqlite3/sqlite3.py
new file mode 100644
index 000000000..299f8247d
--- /dev/null
+++ b/unix-ffi/sqlite3/sqlite3.py
@@ -0,0 +1,213 @@
+import sys
+import ffilib
+import uctypes
+
+
+sq3 = ffilib.open("libsqlite3")
+
+# int sqlite3_open(
+#  const char *filename,   /* Database filename (UTF-8) */
+#  sqlite3 **ppDb          /* OUT: SQLite db handle */
+# );
+sqlite3_open = sq3.func("i", "sqlite3_open", "sp")
+# int sqlite3_config(int, ...);
+sqlite3_config = sq3.func("i", "sqlite3_config", "ii")
+# int sqlite3_get_autocommit(sqlite3*);
+sqlite3_get_autocommit = sq3.func("i", "sqlite3_get_autocommit", "p")
+# int sqlite3_close_v2(sqlite3*);
+sqlite3_close = sq3.func("i", "sqlite3_close_v2", "p")
+# int sqlite3_prepare(
+#  sqlite3 *db,            /* Database handle */
+#  const char *zSql,       /* SQL statement, UTF-8 encoded */
+#  int nByte,              /* Maximum length of zSql in bytes. */
+#  sqlite3_stmt **ppStmt,  /* OUT: Statement handle */
+#  const char **pzTail     /* OUT: Pointer to unused portion of zSql */
+# );
+sqlite3_prepare = sq3.func("i", "sqlite3_prepare_v2", "psipp")
+# int sqlite3_finalize(sqlite3_stmt *pStmt);
+sqlite3_finalize = sq3.func("i", "sqlite3_finalize", "p")
+# int sqlite3_step(sqlite3_stmt*);
+sqlite3_step = sq3.func("i", "sqlite3_step", "p")
+# int sqlite3_column_count(sqlite3_stmt *pStmt);
+sqlite3_column_count = sq3.func("i", "sqlite3_column_count", "p")
+# int sqlite3_column_type(sqlite3_stmt*, int iCol);
+sqlite3_column_type = sq3.func("i", "sqlite3_column_type", "pi")
+# int sqlite3_column_int(sqlite3_stmt*, int iCol);
+sqlite3_column_int = sq3.func("i", "sqlite3_column_int", "pi")
+# double sqlite3_column_double(sqlite3_stmt*, int iCol);
+sqlite3_column_double = sq3.func("d", "sqlite3_column_double", "pi")
+# const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol);
+sqlite3_column_text = sq3.func("s", "sqlite3_column_text", "pi")
+# sqlite3_int64 sqlite3_last_insert_rowid(sqlite3*);
+sqlite3_last_insert_rowid = sq3.func("l", "sqlite3_last_insert_rowid", "p")
+# const char *sqlite3_errmsg(sqlite3*);
+sqlite3_errmsg = sq3.func("s", "sqlite3_errmsg", "p")
+
+
+SQLITE_OK = 0
+SQLITE_ERROR = 1
+SQLITE_BUSY = 5
+SQLITE_MISUSE = 21
+SQLITE_ROW = 100
+SQLITE_DONE = 101
+
+SQLITE_INTEGER = 1
+SQLITE_FLOAT = 2
+SQLITE_TEXT = 3
+SQLITE_BLOB = 4
+SQLITE_NULL = 5
+
+SQLITE_CONFIG_URI = 17
+
+# For compatibility with CPython sqlite3 driver
+LEGACY_TRANSACTION_CONTROL = -1
+
+
+class Error(Exception):
+    pass
+
+
+def check_error(db, s):
+    if s != SQLITE_OK:
+        raise Error(s, sqlite3_errmsg(db))
+
+
+def get_ptr_size():
+    return uctypes.sizeof({"ptr": (0 | uctypes.PTR, uctypes.PTR)})
+
+
+def __prepare_stmt(db, sql):
+    # Prepares a statement
+    stmt_ptr = bytes(get_ptr_size())
+    res = sqlite3_prepare(db, sql, -1, stmt_ptr, None)
+    check_error(db, res)
+    return int.from_bytes(stmt_ptr, sys.byteorder)
+
+def __exec_stmt(db, sql):
+    # Prepares, executes, and finalizes a statement
+    stmt = __prepare_stmt(db, sql)
+    sqlite3_step(stmt)
+    res = sqlite3_finalize(stmt)
+    check_error(db, res)
+
+def __is_dml(sql):
+    # Checks if a sql query is a DML, as these get a BEGIN in LEGACY_TRANSACTION_CONTROL
+    for dml in ["INSERT", "DELETE", "UPDATE", "MERGE"]:
+        if dml in sql.upper():
+            return True
+    return False
+
+
+class Connections:
+    def __init__(self, db, isolation_level, autocommit):
+        self.db = db
+        self.isolation_level = isolation_level
+        self.autocommit = autocommit
+
+    def commit(self):
+        if self.autocommit == LEGACY_TRANSACTION_CONTROL and not sqlite3_get_autocommit(self.db):
+            __exec_stmt(self.db, "COMMIT")
+        elif self.autocommit == False:
+            __exec_stmt(self.db, "COMMIT")
+            __exec_stmt(self.db, "BEGIN")
+
+    def rollback(self):
+        if self.autocommit == LEGACY_TRANSACTION_CONTROL and not sqlite3_get_autocommit(self.db):
+            __exec_stmt(self.db, "ROLLBACK")
+        elif self.autocommit == False:
+            __exec_stmt(self.db, "ROLLBACK")
+            __exec_stmt(self.db, "BEGIN")
+
+    def cursor(self):
+        return Cursor(self.db, self.isolation_level, self.autocommit)
+
+    def close(self):
+        if self.db:
+            if self.autocommit == False and not sqlite3_get_autocommit(self.db):
+                __exec_stmt(self.db, "ROLLBACK")
+
+            res = sqlite3_close(self.db)
+            check_error(self.db, res)
+            self.db = None
+
+
+class Cursor:
+    def __init__(self, db, isolation_level, autocommit):
+        self.db = db
+        self.isolation_level = isolation_level
+        self.autocommit = autocommit
+        self.stmt = None
+
+    def __quote(val):
+        if isinstance(val, str):
+            return "'%s'" % val
+        return str(val)
+
+    def execute(self, sql, params=None):
+        if self.stmt:
+            # If there is an existing statement, finalize that to free it
+            res = sqlite3_finalize(self.stmt)
+            check_error(self.db, res)
+
+        if params:
+            params = [self.__quote(v) for v in params]
+            sql = sql % tuple(params)
+
+        if __is_dml(sql) and self.autocommit == LEGACY_TRANSACTION_CONTROL and sqlite3_get_autocommit(self.db):
+            # For compatibility with CPython, add functionality for their default transaction
+            # behavior. Changing autocommit from LEGACY_TRANSACTION_CONTROL will remove this
+            __exec_stmt(self.db, "BEGIN " + self.isolation_level)
+
+        self.stmt = __prepare_stmt(self.db, sql)
+        self.num_cols = sqlite3_column_count(self.stmt)
+
+        if not self.num_cols:
+            v = self.fetchone()
+            # If it's not select, actually execute it here
+            # num_cols == 0 for statements which don't return data (=> modify it)
+            assert v is None
+            self.lastrowid = sqlite3_last_insert_rowid(self.db)
+
+    def close(self):
+        if self.stmt:
+            res = sqlite3_finalize(self.stmt)
+            check_error(self.db, res)
+            self.stmt = None
+
+    def __make_row(self):
+        res = []
+        for i in range(self.num_cols):
+            t = sqlite3_column_type(self.stmt, i)
+            if t == SQLITE_INTEGER:
+                res.append(sqlite3_column_int(self.stmt, i))
+            elif t == SQLITE_FLOAT:
+                res.append(sqlite3_column_double(self.stmt, i))
+            elif t == SQLITE_TEXT:
+                res.append(sqlite3_column_text(self.stmt, i))
+            else:
+                raise NotImplementedError
+        return tuple(res)
+
+    def fetchone(self):
+        res = sqlite3_step(self.stmt)
+        if res == SQLITE_DONE:
+            return None
+        if res == SQLITE_ROW:
+            return self.__make_row()
+        check_error(self.db, res)
+
+
+def connect(fname, uri=False, isolation_level="", autocommit=LEGACY_TRANSACTION_CONTROL):
+    if isolation_level not in [None, "", "DEFERRED", "IMMEDIATE", "EXCLUSIVE"]:
+        raise Error("Invalid option for isolation level")
+
+    sqlite3_config(SQLITE_CONFIG_URI, int(uri))
+
+    sqlite_ptr = bytes(get_ptr_size())
+    sqlite3_open(fname, sqlite_ptr)
+    db = int.from_bytes(sqlite_ptr, sys.byteorder)
+
+    if autocommit == False:
+        __exec_stmt(db, "BEGIN")
+
+    return Connections(db, isolation_level, autocommit)
diff --git a/unix-ffi/sqlite3/test_sqlite3.py b/unix-ffi/sqlite3/test_sqlite3.py
new file mode 100644
index 000000000..b168f18ff
--- /dev/null
+++ b/unix-ffi/sqlite3/test_sqlite3.py
@@ -0,0 +1,22 @@
+import sqlite3
+
+
+conn = sqlite3.connect(":memory:")
+
+cur = conn.cursor()
+cur.execute("SELECT 1, 'foo', 3.0 UNION SELECT 3, 3, 3")
+
+expected = [(1, "foo", 3.0), (3, 3, 3)]
+
+while True:
+    row = cur.fetchone()
+    if row is None:
+        break
+    print(row)
+    e = expected.pop(0)
+    assert row == e
+
+assert expected == []
+
+cur.close()
+conn.close()
diff --git a/unix-ffi/sqlite3/test_sqlite3_2.py b/unix-ffi/sqlite3/test_sqlite3_2.py
new file mode 100644
index 000000000..515f865c3
--- /dev/null
+++ b/unix-ffi/sqlite3/test_sqlite3_2.py
@@ -0,0 +1,15 @@
+import sqlite3
+
+
+conn = sqlite3.connect(":memory:")
+
+cur = conn.cursor()
+cur.execute("CREATE TABLE foo(a int)")
+cur.execute("INSERT INTO foo VALUES (42)")
+assert cur.lastrowid == 1
+cur.execute("SELECT * FROM foo")
+assert cur.fetchone() == (42,)
+assert cur.fetchone() is None
+
+cur.close()
+conn.close()
diff --git a/unix-ffi/sqlite3/test_sqlite3_3.py b/unix-ffi/sqlite3/test_sqlite3_3.py
new file mode 100644
index 000000000..0a6fefc97
--- /dev/null
+++ b/unix-ffi/sqlite3/test_sqlite3_3.py
@@ -0,0 +1,42 @@
+import sqlite3
+
+
+def test_autocommit():
+    conn = sqlite3.connect(":memory:", autocommit=True)
+
+    # First cursor creates table and inserts value (DML)
+    cur = conn.cursor()
+    cur.execute("CREATE TABLE foo(a int)")
+    cur.execute("INSERT INTO foo VALUES (42)")
+    cur.close()
+
+    # Second cursor fetches 42 due to the autocommit
+    cur = conn.cursor()
+    cur.execute("SELECT * FROM foo")
+    assert cur.fetchone() == (42,)
+    assert cur.fetchone() is None
+
+    cur.close()
+    conn.close()
+
+def test_manual():
+    conn = sqlite3.connect(":memory:", autocommit=False)
+
+    # First cursor creates table, insert rolls back
+    cur = conn.cursor()
+    cur.execute("CREATE TABLE foo(a int)")
+    conn.commit()
+    cur.execute("INSERT INTO foo VALUES (42)")
+    cur.close()
+    conn.rollback()
+
+    # Second connection fetches nothing due to the rollback
+    cur = conn.cursor()
+    cur.execute("SELECT * FROM foo")
+    assert cur.fetchone() is None
+
+    cur.close()
+    conn.close()
+
+test_autocommit()
+test_manual()
diff --git a/unix-ffi/test.support/manifest.py b/unix-ffi/test.support/manifest.py
new file mode 100644
index 000000000..d927e7ee8
--- /dev/null
+++ b/unix-ffi/test.support/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="0.1.3")
+
+package("test")
diff --git a/unix-ffi/test.support/test/support.py b/unix-ffi/test.support/test/support.py
new file mode 100644
index 000000000..435fa9224
--- /dev/null
+++ b/unix-ffi/test.support/test/support.py
@@ -0,0 +1,74 @@
+import sys
+import io
+import unittest
+import gc
+import contextlib
+
+
+TESTFN = "@test"
+
+
+def run_unittest(*classes):
+    suite = unittest.TestSuite()
+    for c in classes:
+        if isinstance(c, str):
+            c = __import__(c)
+            for name in dir(c):
+                obj = getattr(c, name)
+                if isinstance(obj, type) and issubclass(obj, unittest.TestCase):
+                    suite.addTest(obj)
+        else:
+            suite.addTest(c)
+    runner = unittest.TestRunner()
+    result = runner.run(suite)
+
+
+def can_symlink():
+    return False
+
+
+def skip_unless_symlink(test):
+    """Skip decorator for tests that require functional symlink"""
+    ok = can_symlink()
+    msg = "Requires functional symlink implementation"
+    return test if ok else unittest.skip(msg)(test)
+
+
+def create_empty_file(name):
+    open(name, "w").close()
+
+
+@contextlib.contextmanager
+def disable_gc():
+    have_gc = gc.isenabled()
+    gc.disable()
+    try:
+        yield
+    finally:
+        if have_gc:
+            gc.enable()
+
+
+def gc_collect():
+    gc.collect()
+    gc.collect()
+    gc.collect()
+
+
+@contextlib.contextmanager
+def captured_output(stream_name):
+    org = getattr(sys, stream_name)
+    buf = io.StringIO()
+    setattr(sys, stream_name, buf)
+    try:
+        yield buf
+    finally:
+        setattr(sys, stream_name, org)
+
+
+def captured_stderr():
+    return captured_output("stderr")
+
+
+def requires_IEEE_754(f):
+    return f
diff --git a/time/example_strftime.py b/unix-ffi/time/example_strftime.py
similarity index 100%
rename from time/example_strftime.py
rename to unix-ffi/time/example_strftime.py
diff --git a/unix-ffi/time/example_time_tuple.py b/unix-ffi/time/example_time_tuple.py
new file mode 100644
index 000000000..2c73ed272
--- /dev/null
+++ b/unix-ffi/time/example_time_tuple.py
@@ -0,0 +1,10 @@
+import time
+
+ts = 0
+for i in range(0, 31):
+    ts += 1 << i
+    ltt = time.localtime(ts)
+    gtt = time.gmtime(ts)
+    print("ts: ", ts, " local: ", tuple(ltt), " utc: ", tuple(gtt))
+    lts = time.mktime(ltt)
+    assert ts == lts
diff --git a/unix-ffi/time/manifest.py b/unix-ffi/time/manifest.py
new file mode 100644
index 000000000..d1ff709a4
--- /dev/null
+++ b/unix-ffi/time/manifest.py
@@ -0,0 +1,5 @@
+metadata(version="0.5.0")
+
+require("ffilib")
+
+module("time.py")
diff --git a/unix-ffi/time/test_strftime.py b/unix-ffi/time/test_strftime.py
new file mode 100644
index 000000000..fc2daebb6
--- /dev/null
+++ b/unix-ffi/time/test_strftime.py
@@ -0,0 +1,36 @@
+from time import strftime
+
+# These tuples were generated using localtime() and gmtime() in CPython.
+INPUT = (
+    (2017, 1, 1, 23, 40, 39, 6, 222, 1),
+    (2010, 2, 28, 9, 59, 60, 1, 111, 0),
+    (2000, 3, 31, 1, 33, 0, 2, 44, 1),
+    (2020, 4, 30, 21, 22, 59, 3, 234, 0),
+    (1977, 5, 15, 23, 55, 1, 4, 123, 1),
+    (1940, 6, 11, 9, 21, 33, 5, 55, 0),
+    (1918, 7, 24, 6, 12, 44, 7, 71, 1),
+    (1800, 8, 17, 0, 59, 55, 3, 89, 0),
+    (2222, 9, 5, 1, 0, 4, 2, 255, 1),
+    (2017, 10, 10, 9, 1, 5, 6, 200, 0),
+    (2016, 11, 7, 18, 8, 16, 7, 100, 1),
+    (2001, 12, 2, 12, 19, 27, 1, 33, 0),
+)
+
+# These values were generated using strftime() in CPython.
+EXPECTED = (
+    ("20170101234039"),
+    ("20100228095960"),
+    ("20000331013300"),
+    ("20200430212259"),
+    ("19770515235501"),
+    ("19400611092133"),
+    ("19180724061244"),
+    ("18000817005955"),
+    ("22220905010004"),
+    ("20171010090105"),
+    ("20161107180816"),
+    ("20011202121927"),
+)
+
+for i in range(len(INPUT)):
+    assert strftime("%Y%m%d%H%M%S", INPUT[i]) == EXPECTED[i]
diff --git a/unix-ffi/time/time.py b/unix-ffi/time/time.py
new file mode 100644
index 000000000..319228dc8
--- /dev/null
+++ b/unix-ffi/time/time.py
@@ -0,0 +1,95 @@
+from utime import *
+from collections import namedtuple
+import struct
+import uctypes
+import ffi
+import ffilib
+import array
+
+libc = ffilib.libc()
+
+# struct tm *gmtime(const time_t *timep);
+# struct tm *localtime(const time_t *timep);
+# size_t strftime(char *s, size_t max, const char *format,
+#                       const struct tm *tm);
+gmtime_ = libc.func("P", "gmtime", "P")
+localtime_ = libc.func("P", "localtime", "P")
+strftime_ = libc.func("i", "strftime", "sisP")
+mktime_ = libc.func("i", "mktime", "P")
+
+_struct_time = namedtuple(
+    "struct_time",
+    [
+        "tm_year",
+        "tm_mon",
+        "tm_mday",
+        "tm_hour",
+        "tm_min",
+        "tm_sec",
+        "tm_wday",
+        "tm_yday",
+        "tm_isdst",
+    ],
+)
+
+
+def _tuple_to_c_tm(t):
+    return struct.pack(
+        "@iiiiiiiii", t[5], t[4], t[3], t[2], t[1] - 1, t[0] - 1900, (t[6] + 1) % 7, t[7] - 1, t[8]
+    )
+
+
+def _c_tm_to_tuple(tm):
+    t = struct.unpack("@iiiiiiiii", tm)
+    return _struct_time(
+        t[5] + 1900, t[4] + 1, t[3], t[2], t[1], t[0], (t[6] - 1) % 7, t[7] + 1, t[8]
+    )
+
+
+def struct_time(tm):
+    return _struct_time(*tm)
+
+
+def strftime(format, t=None):
+    if t is None:
+        t = localtime()
+
+    buf = bytearray(32)
+    l = strftime_(buf, 32, format, _tuple_to_c_tm(t))
+    return str(buf[:l], "utf-8")
+
+
+def localtime(t=None):
+    if t is None:
+        t = time()
+
+    t = int(t)
+    a = struct.pack("l", t)
+    tm_p = localtime_(a)
+    return _c_tm_to_tuple(uctypes.bytearray_at(tm_p, 36))
+
+
+def gmtime(t=None):
+    if t is None:
+        t = time()
+
+    t = int(t)
+    a = struct.pack("l", t)
+    tm_p = gmtime_(a)
+    return _c_tm_to_tuple(uctypes.bytearray_at(tm_p, 36))
+
+
+def mktime(tt):
+    return mktime_(_tuple_to_c_tm(tt))
+
+
+def perf_counter():
+    return time()
+
+
+def process_time():
+    return clock()
+
+
+daylight = 0
+timezone = 0
diff --git a/unix-ffi/timeit/manifest.py b/unix-ffi/timeit/manifest.py
new file mode 100644
index 000000000..ea13af331
--- /dev/null
+++ b/unix-ffi/timeit/manifest.py
@@ -0,0 +1,9 @@
+metadata(version="3.3.4")
+
+require("getopt")
+require("itertools")
+# require("linecache") TODO
+require("time")
+require("traceback")
+
+module("timeit.py")
diff --git a/timeit/timeit.py b/unix-ffi/timeit/timeit.py
similarity index 91%
rename from timeit/timeit.py
rename to unix-ffi/timeit/timeit.py
index cfec60b40..fabcd51b4 100644
--- a/timeit/timeit.py
+++ b/unix-ffi/timeit/timeit.py
@@ -56,6 +56,7 @@
 import gc
 import sys
 import time
+
 try:
     import itertools
 except ImportError:
@@ -82,12 +83,15 @@ def inner(_it, _timer):
     return _t1 - _t0
 """
 
+
 def reindent(src, indent):
     """Helper to reindent a multi-line statement."""
-    return src.replace("\n", "\n" + " "*indent)
+    return src.replace("\n", "\n" + " " * indent)
+
 
 def _template_func(setup, func):
     """Create a timer function. Used if the "statement" is a callable."""
+
     def inner(_it, _timer, _func=func):
         setup()
         _t0 = _timer()
@@ -95,8 +99,10 @@ def inner(_it, _timer, _func=func):
             _func()
         _t1 = _timer()
         return _t1 - _t0
+
     return inner
 
+
 class Timer:
     """Class for timing execution speed of small code snippets.
 
@@ -123,11 +129,11 @@ def __init__(self, stmt="pass", setup="pass", timer=default_timer):
                 setup = reindent(setup, 4)
                 src = template.format(stmt=stmt, setup=setup)
             elif callable(setup):
-                src = template.format(stmt=stmt, setup='_setup()')
-                ns['_setup'] = setup
+                src = template.format(stmt=stmt, setup="_setup()")
+                ns["_setup"] = setup
             else:
                 raise ValueError("setup is neither a string nor callable")
-            self.src = src # Save for traceback display
+            self.src = src  # Save for traceback display
             code = compile(src, dummy_src_name, "exec")
             exec(code, globals(), ns)
             self.inner = ns["inner"]
@@ -135,8 +141,10 @@ def __init__(self, stmt="pass", setup="pass", timer=default_timer):
             self.src = None
             if isinstance(setup, str):
                 _setup = setup
+
                 def setup():
                     exec(_setup, globals(), ns)
+
             elif not callable(setup):
                 raise ValueError("setup is neither a string nor callable")
             self.inner = _template_func(setup, stmt)
@@ -161,11 +169,14 @@ def print_exc(self, file=None):
         sent; it defaults to sys.stderr.
         """
         import linecache, traceback
+
         if self.src is not None:
-            linecache.cache[dummy_src_name] = (len(self.src),
-                                               None,
-                                               self.src.split("\n"),
-                                               dummy_src_name)
+            linecache.cache[dummy_src_name] = (
+                len(self.src),
+                None,
+                self.src.split("\n"),
+                dummy_src_name,
+            )
         # else the source is already stored somewhere else
 
         traceback.print_exc(file=file)
@@ -185,7 +196,7 @@ def timeit(self, number=default_number):
         else:
             it = [None] * number
         gcold = gc.isenabled()
-        gc.disable()
+        #        gc.disable()
         try:
             timing = self.inner(it, self.timer)
         finally:
@@ -219,16 +230,19 @@ def repeat(self, repeat=default_repeat, number=default_number):
             r.append(t)
         return r
 
-def timeit(stmt="pass", setup="pass", timer=default_timer,
-           number=default_number):
+
+def timeit(stmt="pass", setup="pass", timer=default_timer, number=default_number):
     """Convenience function to create Timer object and call timeit method."""
     return Timer(stmt, setup, timer).timeit(number)
 
-def repeat(stmt="pass", setup="pass", timer=default_timer,
-           repeat=default_repeat, number=default_number):
+
+def repeat(
+    stmt="pass", setup="pass", timer=default_timer, repeat=default_repeat, number=default_number
+):
     """Convenience function to create Timer object and call repeat method."""
     return Timer(stmt, setup, timer).repeat(repeat, number)
 
+
 def main(args=None, *, _wrap_timer=None):
     """Main program, used when run as a script.
 
@@ -249,18 +263,20 @@ def main(args=None, *, _wrap_timer=None):
     if args is None:
         args = sys.argv[1:]
     import getopt
+
     try:
-        opts, args = getopt.getopt(args, "n:s:r:tcpvh",
-                                   ["number=", "setup=", "repeat=",
-                                    "time", "clock", "process",
-                                    "verbose", "help"])
+        opts, args = getopt.getopt(
+            args,
+            "n:s:r:tcpvh",
+            ["number=", "setup=", "repeat=", "time", "clock", "process", "verbose", "help"],
+        )
     except getopt.error as err:
         print(err)
         print("use -h/--help for command line help")
         return 2
     timer = default_timer
     stmt = "\n".join(args) or "pass"
-    number = 0 # auto-determine
+    number = 0  # auto-determine
     setup = []
     repeat = default_repeat
     verbose = 0
@@ -285,13 +301,14 @@ def main(args=None, *, _wrap_timer=None):
                 precision += 1
             verbose += 1
         if o in ("-h", "--help"):
-            print(__doc__, end=' ')
+            print(__doc__, end=" ")
             return 0
     setup = "\n".join(setup) or "pass"
     # Include the current directory, so that local imports work (sys.path
     # contains the directory of this script, rather than the current
     # directory)
     import os
+
     sys.path.insert(0, os.curdir)
     if _wrap_timer is not None:
         timer = _wrap_timer(timer)
@@ -317,7 +334,7 @@ def main(args=None, *, _wrap_timer=None):
     best = min(r)
     if verbose:
         print("raw times:", " ".join(["%.*g" % (precision, x) for x in r]))
-    print("%d loops," % number, end=' ')
+    print("%d loops," % number, end=" ")
     usec = best * 1e6 / number
     if usec < 1000:
         print("best of %d: %.*g usec per loop" % (repeat, precision, usec))
@@ -330,5 +347,6 @@ def main(args=None, *, _wrap_timer=None):
             print("best of %d: %.*g sec per loop" % (repeat, precision, sec))
     return None
 
+
 if __name__ == "__main__":
     sys.exit(main())
diff --git a/unix-ffi/tty/manifest.py b/unix-ffi/tty/manifest.py
new file mode 100644
index 000000000..7c8a88cac
--- /dev/null
+++ b/unix-ffi/tty/manifest.py
@@ -0,0 +1,3 @@
+metadata(version="1.0.1")
+
+module("tty.py")
diff --git a/tty/tty.py b/unix-ffi/tty/tty.py
similarity index 100%
rename from tty/tty.py
rename to unix-ffi/tty/tty.py
diff --git a/unix-ffi/ucurses/manifest.py b/unix-ffi/ucurses/manifest.py
new file mode 100644
index 000000000..8ec2675a5
--- /dev/null
+++ b/unix-ffi/ucurses/manifest.py
@@ -0,0 +1,9 @@
+metadata(version="0.1.2")
+
+# Originally written by Paul Sokolovsky.
+
+require("os")
+require("tty")
+require("select")
+
+package("ucurses")
diff --git a/unix-ffi/ucurses/ucurses/__init__.py b/unix-ffi/ucurses/ucurses/__init__.py
new file mode 100644
index 000000000..688df745c
--- /dev/null
+++ b/unix-ffi/ucurses/ucurses/__init__.py
@@ -0,0 +1,341 @@
+import os
+import tty, termios
+import select
+
+COLOR_BLACK = 0
+COLOR_RED = 1
+COLOR_GREEN = 2
+COLOR_YELLOW = 3
+COLOR_BLUE = 4
+COLOR_MAGENTA = 5
+COLOR_CYAN = 6
+COLOR_WHITE = 7
+
+A_NORMAL = 0
+A_BOLD = 1
+A_UNDERLINE = 2
+A_REVERSE = 4
+A_STANDOUT = A_REVERSE
+
+ATTRMAP = {
+    A_NORMAL: b"\x1b[0m",
+    A_BOLD: b"\x1b[1m",  # Some terminal emulators don't render bold by default, then use 4m for underline
+    A_REVERSE: b"\x1b[7m",
+}
+
+# Use http://www.utf8-chartable.de/unicode-utf8-table.pl
+# for utf-8 pseudographic reference
+# "─"
+ACS_HLINE = b"\xe2\x94\x80"
+# "│"
+ACS_VLINE = b"\xe2\x94\x82"
+# "┌"
+ACS_ULCORNER = b"\xe2\x94\x8c"
+# "┐"
+ACS_URCORNER = b"\xe2\x94\x90"
+# "└"
+ACS_LLCORNER = b"\xe2\x94\x94"
+# "┘"
+ACS_LRCORNER = b"\xe2\x94\x98"
+
+KEY_F1 = 1031
+KEY_RESIZE = 1100
+KEY_MOUSE = 1101
+KEY_BTAB = 1090
+
+KEY_UP = 1001
+KEY_DOWN = 1002
+KEY_LEFT = 1003
+KEY_RIGHT = 1004
+KEY_HOME = 1005
+KEY_END = 1006
+KEY_PGUP = 1007
+KEY_PGDN = 1008
+KEY_QUIT = 1009
+KEY_ENTER = 1010
+KEY_BACKSPACE = 1011
+KEY_DELETE = 1012
+KEY_ESC = 0x1B
+
+KEY_DC = KEY_DELETE
+KEY_PPAGE = KEY_PGUP
+KEY_NPAGE = KEY_PGDN
+
+KEYMAP = {
+    b"\x1b[A": KEY_UP,
+    b"\x1b[B": KEY_DOWN,
+    b"\x1b[D": KEY_LEFT,
+    b"\x1b[C": KEY_RIGHT,
+    b"\x1bOH": KEY_HOME,
+    b"\x1bOF": KEY_END,
+    b"\x1b[1~": KEY_HOME,
+    b"\x1b[4~": KEY_END,
+    b"\x1b[5~": KEY_PGUP,
+    b"\x1b[6~": KEY_PGDN,
+    b"\x03": KEY_QUIT,
+    b"\r": KEY_ENTER,
+    b"\x7f": KEY_BACKSPACE,
+    b"\x1b[3~": KEY_DELETE,
+    b"\x1bOA": KEY_UP,
+    b"\x1bOB": KEY_DOWN,
+    b"\x1bOD": KEY_LEFT,
+    b"\x1bOC": KEY_RIGHT,
+    b"\x1bOP": KEY_F1,
+    b"\x1b": KEY_ESC,
+    b"\x1b[Z": KEY_BTAB,
+}
+
+ALL_MOUSE_EVENTS = 0xFF
+
+
+def _wr(s):
+    # TODO: When Python is 3.5, update this to use only bytes
+    if isinstance(s, str):
+        s = bytes(s, "utf-8")
+    os.write(1, s)
+
+
+def _move(row, col):
+    # TODO: When Python is 3.5, update this to use bytes
+    _wr("\x1b[%d;%dH" % (row + 1, col + 1))
+
+
+# Clear specified number of positions
+def _clear_num_pos(num):
+    if num > 0:
+        _wr("\x1b[%dX" % num)
+
+
+def _draw_box(left, top, width, height):
+    bottom = top + height - 1
+    _move(top, left)
+    _wr(ACS_ULCORNER)
+    hor = ACS_HLINE * (width - 2)
+    _wr(hor)
+    _wr(ACS_URCORNER)
+
+    _move(bottom, left)
+    _wr(ACS_LLCORNER)
+    _wr(hor)
+    _wr(ACS_LRCORNER)
+
+    top += 1
+    while top < bottom:
+        _move(top, left)
+        _wr(ACS_VLINE)
+        _move(top, left + width - 1)
+        _wr(ACS_VLINE)
+        top += 1
+
+
+class error(Exception):
+    pass
+
+
+class Window:
+    def __init__(self, lines, cols, y, x):
+        self.lines = lines
+        self.cols = cols
+        self.y = y
+        self.x = x
+        self.bkgattr = A_NORMAL
+        self.keybuf = None
+        self.keyi = 0
+        self.keydelay = -1
+
+    def _goto(self, row, col):
+        _move(self.y + row, self.x + col)
+
+    def move(self, y, x):
+        # Maybe need to cache coords?
+        self._goto(y, x)
+
+    def getmaxyx(self):
+        return (self.lines, self.cols)
+
+    def addstr(self, y, x, str, attr=A_NORMAL):
+        self._goto(y, x)
+        # TODO: Should be "ORed"
+        if attr == A_NORMAL:
+            attr = self.bkgattr
+        if attr != A_NORMAL:
+            _wr(ATTRMAP[attr])
+            _wr(str)
+            _wr(ATTRMAP[A_NORMAL])
+        else:
+            _wr(str)
+
+    def addnstr(self, y, x, str, n, attr=A_NORMAL):
+        self.addstr(y, x, str[:n], attr)
+
+    def addch(self, y, x, ch, attr=A_NORMAL):
+        if isinstance(ch, int):
+            ch = chr(ch)
+        self.addstr(y, x, ch, attr)
+
+    def attron(self, attr):
+        pass
+
+    def attroff(self, attr):
+        pass
+
+    def attrset(self, attr):
+        pass
+
+    def bkgdset(self, ch, attr=A_NORMAL):
+        self.bkgattr = attr
+
+    def erase(self):
+        for i in range(self.lines):
+            self._goto(i, 0)
+            _clear_num_pos(self.cols)
+
+    def border(self):
+        _draw_box(self.x, self.y, self.cols, self.lines)
+
+    def hline(self, y, x, ch, n):
+        self.move(y, x)
+        _wr(ch * n)
+
+    def vline(self, y, x, ch, n):
+        for i in range(n):
+            self.move(y + i, x)
+            _wr(ch)
+
+    def refresh(self):
+        pass
+
+    def redrawwin(self):
+        pass
+
+    def keypad(self, yes):
+        pass
+
+    def timeout(self, delay):
+        self.keydelay = delay
+
+    def nodelay(self, yes):
+        if yes:
+            self.keydelay = 0
+        else:
+            self.keydelay = -1
+
+    def getch(self):
+        if self.keybuf and self.keyi < len(self.keybuf):
+            c = self.keybuf[self.keyi]
+            self.keyi += 1
+            return c
+
+        if self.keydelay >= 0:
+            USE_EPOLL = 1
+            if USE_EPOLL:
+                poll = select.epoll()
+                poll.register(0, select.EPOLLIN)
+                res = poll.poll(self.keydelay / 1000)
+                poll.unregister(0)
+                poll.close()
+            else:
+                res = select.select([0], [], [], self.keydelay / 1000)[0]
+            if not res:
+                return -1
+
+        key = os.read(0, 32)
+        if key[0] != 0x1B:
+            self.keybuf = key
+            self.keyi = 1
+            key = key[0]
+
+        else:
+            if key in KEYMAP:
+                key = KEYMAP[key]
+            else:
+                assert False, repr(key)
+        return key
+
+
+SCREEN = Window(24, 80, 0, 0)
+org_termios = None
+
+
+def wrapper(func):
+    global org_termios
+    org_termios = termios.tcgetattr(0)
+    res = func()
+    endwin()
+    return res
+
+
+def initscr():
+    global org_termios
+    org_termios = termios.tcgetattr(0)
+    return SCREEN
+
+
+def doupdate():
+    pass
+
+
+def endwin():
+    global org_termios
+    _wr(b"\r")
+    termios.tcsetattr(0, termios.TCSANOW, org_termios)
+
+
+def raw():
+    tty.setraw(0)
+
+
+def cbreak():
+    # TODO
+    pass
+
+
+def nocbreak():
+    # TODO
+    pass
+
+
+def echo():
+    # TODO
+    pass
+
+
+def noecho():
+    # TODO
+    pass
+
+
+def meta(yes):
+    # TODO
+    pass
+
+
+def mousemask(mask):
+    # Mouse reporting - X10 compatbility mode
+    _wr(b"\x1b[?9h")
+
+
+def has_colors():
+    return False
+
+
+def can_change_color():
+    return False
+
+
+def curs_set(visibility):
+    if visibility > 0:
+        _wr(b"\x1b[?25h")
+    else:
+        _wr(b"\x1b[?25l")
+
+
+def beep():
+    _wr(b"\x07")
+
+
+def newwin(lines, cols, y=0, x=0):
+    # print("newwin(%d, %d, %d, %d)" % (lines, cols, y, x))
+    cols = cols or SCREEN.cols
+    lines = lines or SCREEN.lines
+    return Window(lines, cols, y, x)
diff --git a/unix-ffi/urllib.parse/manifest.py b/unix-ffi/urllib.parse/manifest.py
new file mode 100644
index 000000000..94109b134
--- /dev/null
+++ b/unix-ffi/urllib.parse/manifest.py
@@ -0,0 +1,7 @@
+metadata(version="0.5.2")
+
+require("re")
+require("collections")
+require("collections-defaultdict")
+
+package("urllib")
diff --git a/unix-ffi/urllib.parse/test_urlparse.py b/unix-ffi/urllib.parse/test_urlparse.py
new file mode 100644
index 000000000..477bbce4e
--- /dev/null
+++ b/unix-ffi/urllib.parse/test_urlparse.py
@@ -0,0 +1,921 @@
+#! /usr/bin/env python3
+
+from test import support
+import unittest
+import urllib.parse
+
+RFC1808_BASE = "http://a/b/c/d;p?q#f"
+RFC2396_BASE = "http://a/b/c/d;p?q"
+RFC3986_BASE = "http://a/b/c/d;p?q"
+SIMPLE_BASE = "http://a/b/c/d"
+
+# A list of test cases.  Each test case is a two-tuple that contains
+# a string with the query and a dictionary with the expected result.
+
+parse_qsl_test_cases = [
+    ("", []),
+    ("&", []),
+    ("&&", []),
+    ("=", [("", "")]),
+    ("=a", [("", "a")]),
+    ("a", [("a", "")]),
+    ("a=", [("a", "")]),
+    ("a=", [("a", "")]),
+    ("&a=b", [("a", "b")]),
+    ("a=a+b&b=b+c", [("a", "a b"), ("b", "b c")]),
+    ("a=1&a=2", [("a", "1"), ("a", "2")]),
+    (b"", []),
+    (b"&", []),
+    (b"&&", []),
+    (b"=", [(b"", b"")]),
+    (b"=a", [(b"", b"a")]),
+    (b"a", [(b"a", b"")]),
+    (b"a=", [(b"a", b"")]),
+    (b"a=", [(b"a", b"")]),
+    (b"&a=b", [(b"a", b"b")]),
+    (b"a=a+b&b=b+c", [(b"a", b"a b"), (b"b", b"b c")]),
+    (b"a=1&a=2", [(b"a", b"1"), (b"a", b"2")]),
+]
+
+
+class UrlParseTestCase(unittest.TestCase):
+    def checkRoundtrips(self, url, parsed, split):
+        result = urllib.parse.urlparse(url)
+        self.assertEqual(result, parsed)
+        t = (
+            result.scheme,
+            result.netloc,
+            result.path,
+            result.params,
+            result.query,
+            result.fragment,
+        )
+        self.assertEqual(t, parsed)
+        # put it back together and it should be the same
+        result2 = urllib.parse.urlunparse(result)
+        self.assertEqual(result2, url)
+        self.assertEqual(result2, result.geturl())
+
+        # the result of geturl() is a fixpoint; we can always parse it
+        # again to get the same result:
+        result3 = urllib.parse.urlparse(result.geturl())
+        self.assertEqual(result3.geturl(), result.geturl())
+        self.assertEqual(result3, result)
+        self.assertEqual(result3.scheme, result.scheme)
+        self.assertEqual(result3.netloc, result.netloc)
+        self.assertEqual(result3.path, result.path)
+        self.assertEqual(result3.params, result.params)
+        self.assertEqual(result3.query, result.query)
+        self.assertEqual(result3.fragment, result.fragment)
+        self.assertEqual(result3.username, result.username)
+        self.assertEqual(result3.password, result.password)
+        self.assertEqual(result3.hostname, result.hostname)
+        self.assertEqual(result3.port, result.port)
+
+        # check the roundtrip using urlsplit() as well
+        result = urllib.parse.urlsplit(url)
+        self.assertEqual(result, split)
+        t = (result.scheme, result.netloc, result.path, result.query, result.fragment)
+        self.assertEqual(t, split)
+        result2 = urllib.parse.urlunsplit(result)
+        self.assertEqual(result2, url)
+        self.assertEqual(result2, result.geturl())
+
+        # check the fixpoint property of re-parsing the result of geturl()
+        result3 = urllib.parse.urlsplit(result.geturl())
+        self.assertEqual(result3.geturl(), result.geturl())
+        self.assertEqual(result3, result)
+        self.assertEqual(result3.scheme, result.scheme)
+        self.assertEqual(result3.netloc, result.netloc)
+        self.assertEqual(result3.path, result.path)
+        self.assertEqual(result3.query, result.query)
+        self.assertEqual(result3.fragment, result.fragment)
+        self.assertEqual(result3.username, result.username)
+        self.assertEqual(result3.password, result.password)
+        self.assertEqual(result3.hostname, result.hostname)
+        self.assertEqual(result3.port, result.port)
+
+    def test_qsl(self):
+        for orig, expect in parse_qsl_test_cases:
+            result = urllib.parse.parse_qsl(orig, keep_blank_values=True)
+            self.assertEqual(result, expect, "Error parsing %r" % orig)
+            expect_without_blanks = [v for v in expect if len(v[1])]
+            result = urllib.parse.parse_qsl(orig, keep_blank_values=False)
+            self.assertEqual(result, expect_without_blanks, "Error parsing %r" % orig)
+
+    def test_roundtrips(self):
+        str_cases = [
+            (
+                "file:///tmp/junk.txt",
+                ("file", "", "/tmp/junk.txt", "", "", ""),
+                ("file", "", "/tmp/junk.txt", "", ""),
+            ),
+            (
+                "imap://mail.python.org/mbox1",
+                ("imap", "mail.python.org", "/mbox1", "", "", ""),
+                ("imap", "mail.python.org", "/mbox1", "", ""),
+            ),
+            (
+                "mms://wms.sys.hinet.net/cts/Drama/09006251100.asf",
+                ("mms", "wms.sys.hinet.net", "/cts/Drama/09006251100.asf", "", "", ""),
+                ("mms", "wms.sys.hinet.net", "/cts/Drama/09006251100.asf", "", ""),
+            ),
+            (
+                "nfs://server/path/to/file.txt",
+                ("nfs", "server", "/path/to/file.txt", "", "", ""),
+                ("nfs", "server", "/path/to/file.txt", "", ""),
+            ),
+            (
+                "svn+ssh://svn.zope.org/repos/main/ZConfig/trunk/",
+                ("svn+ssh", "svn.zope.org", "/repos/main/ZConfig/trunk/", "", "", ""),
+                ("svn+ssh", "svn.zope.org", "/repos/main/ZConfig/trunk/", "", ""),
+            ),
+            (
+                "git+ssh://git@github.com/user/project.git",
+                ("git+ssh", "git@github.com", "/user/project.git", "", "", ""),
+                ("git+ssh", "git@github.com", "/user/project.git", "", ""),
+            ),
+        ]
+
+        def _encode(t):
+            return (
+                t[0].encode("ascii"),
+                tuple(x.encode("ascii") for x in t[1]),
+                tuple(x.encode("ascii") for x in t[2]),
+            )
+
+        bytes_cases = [_encode(x) for x in str_cases]
+        for url, parsed, split in str_cases + bytes_cases:
+            self.checkRoundtrips(url, parsed, split)
+
+    def test_http_roundtrips(self):
+        # urllib.parse.urlsplit treats 'http:' as an optimized special case,
+        # so we test both 'http:' and 'https:' in all the following.
+        # Three cheers for white box knowledge!
+        str_cases = [
+            (
+                "://www.python.org",
+                ("www.python.org", "", "", "", ""),
+                ("www.python.org", "", "", ""),
+            ),
+            (
+                "://www.python.org#abc",
+                ("www.python.org", "", "", "", "abc"),
+                ("www.python.org", "", "", "abc"),
+            ),
+            (
+                "://www.python.org?q=abc",
+                ("www.python.org", "", "", "q=abc", ""),
+                ("www.python.org", "", "q=abc", ""),
+            ),
+            (
+                "://www.python.org/#abc",
+                ("www.python.org", "/", "", "", "abc"),
+                ("www.python.org", "/", "", "abc"),
+            ),
+            ("://a/b/c/d;p?q#f", ("a", "/b/c/d", "p", "q", "f"), ("a", "/b/c/d;p", "q", "f")),
+        ]
+
+        def _encode(t):
+            return (
+                t[0].encode("ascii"),
+                tuple(x.encode("ascii") for x in t[1]),
+                tuple(x.encode("ascii") for x in t[2]),
+            )
+
+        bytes_cases = [_encode(x) for x in str_cases]
+        str_schemes = ("http", "https")
+        bytes_schemes = (b"http", b"https")
+        str_tests = str_schemes, str_cases
+        bytes_tests = bytes_schemes, bytes_cases
+        for schemes, test_cases in (str_tests, bytes_tests):
+            for scheme in schemes:
+                for url, parsed, split in test_cases:
+                    url = scheme + url
+                    parsed = (scheme,) + parsed
+                    split = (scheme,) + split
+                    self.checkRoundtrips(url, parsed, split)
+
+    def checkJoin(self, base, relurl, expected):
+        str_components = (base, relurl, expected)
+        self.assertEqual(urllib.parse.urljoin(base, relurl), expected)
+        bytes_components = baseb, relurlb, expectedb = [x.encode("ascii") for x in str_components]
+        self.assertEqual(urllib.parse.urljoin(baseb, relurlb), expectedb)
+
+    def test_unparse_parse(self):
+        str_cases = [
+            "Python",
+            "./Python",
+            "x-newscheme://foo.com/stuff",
+            "x://y",
+            "x:/y",
+            "x:/",
+            "/",
+        ]
+        bytes_cases = [x.encode("ascii") for x in str_cases]
+        for u in str_cases + bytes_cases:
+            self.assertEqual(urllib.parse.urlunsplit(urllib.parse.urlsplit(u)), u)
+            self.assertEqual(urllib.parse.urlunparse(urllib.parse.urlparse(u)), u)
+
+    def test_RFC1808(self):
+        # "normal" cases from RFC 1808:
+        self.checkJoin(RFC1808_BASE, "g:h", "g:h")
+        self.checkJoin(RFC1808_BASE, "g", "http://a/b/c/g")
+        self.checkJoin(RFC1808_BASE, "./g", "http://a/b/c/g")
+        self.checkJoin(RFC1808_BASE, "g/", "http://a/b/c/g/")
+        self.checkJoin(RFC1808_BASE, "/g", "http://a/g")
+        self.checkJoin(RFC1808_BASE, "//g", "http://g")
+        self.checkJoin(RFC1808_BASE, "g?y", "http://a/b/c/g?y")
+        self.checkJoin(RFC1808_BASE, "g?y/./x", "http://a/b/c/g?y/./x")
+        self.checkJoin(RFC1808_BASE, "#s", "http://a/b/c/d;p?q#s")
+        self.checkJoin(RFC1808_BASE, "g#s", "http://a/b/c/g#s")
+        self.checkJoin(RFC1808_BASE, "g#s/./x", "http://a/b/c/g#s/./x")
+        self.checkJoin(RFC1808_BASE, "g?y#s", "http://a/b/c/g?y#s")
+        self.checkJoin(RFC1808_BASE, "g;x", "http://a/b/c/g;x")
+        self.checkJoin(RFC1808_BASE, "g;x?y#s", "http://a/b/c/g;x?y#s")
+        self.checkJoin(RFC1808_BASE, ".", "http://a/b/c/")
+        self.checkJoin(RFC1808_BASE, "./", "http://a/b/c/")
+        self.checkJoin(RFC1808_BASE, "..", "http://a/b/")
+        self.checkJoin(RFC1808_BASE, "../", "http://a/b/")
+        self.checkJoin(RFC1808_BASE, "../g", "http://a/b/g")
+        self.checkJoin(RFC1808_BASE, "../..", "http://a/")
+        self.checkJoin(RFC1808_BASE, "../../", "http://a/")
+        self.checkJoin(RFC1808_BASE, "../../g", "http://a/g")
+
+        # "abnormal" cases from RFC 1808:
+        self.checkJoin(RFC1808_BASE, "", "http://a/b/c/d;p?q#f")
+        self.checkJoin(RFC1808_BASE, "../../../g", "http://a/../g")
+        self.checkJoin(RFC1808_BASE, "../../../../g", "http://a/../../g")
+        self.checkJoin(RFC1808_BASE, "/./g", "http://a/./g")
+        self.checkJoin(RFC1808_BASE, "/../g", "http://a/../g")
+        self.checkJoin(RFC1808_BASE, "g.", "http://a/b/c/g.")
+        self.checkJoin(RFC1808_BASE, ".g", "http://a/b/c/.g")
+        self.checkJoin(RFC1808_BASE, "g..", "http://a/b/c/g..")
+        self.checkJoin(RFC1808_BASE, "..g", "http://a/b/c/..g")
+        self.checkJoin(RFC1808_BASE, "./../g", "http://a/b/g")
+        self.checkJoin(RFC1808_BASE, "./g/.", "http://a/b/c/g/")
+        self.checkJoin(RFC1808_BASE, "g/./h", "http://a/b/c/g/h")
+        self.checkJoin(RFC1808_BASE, "g/../h", "http://a/b/c/h")
+
+        # RFC 1808 and RFC 1630 disagree on these (according to RFC 1808),
+        # so we'll not actually run these tests (which expect 1808 behavior).
+        # self.checkJoin(RFC1808_BASE, 'http:g', 'http:g')
+        # self.checkJoin(RFC1808_BASE, 'http:', 'http:')
+
+    def test_RFC2368(self):
+        # Issue 11467: path that starts with a number is not parsed correctly
+        self.assertEqual(
+            urllib.parse.urlparse("mailto:1337@example.org"),
+            ("mailto", "", "1337@example.org", "", "", ""),
+        )
+
+    def test_RFC2396(self):
+        # cases from RFC 2396
+
+        self.checkJoin(RFC2396_BASE, "g:h", "g:h")
+        self.checkJoin(RFC2396_BASE, "g", "http://a/b/c/g")
+        self.checkJoin(RFC2396_BASE, "./g", "http://a/b/c/g")
+        self.checkJoin(RFC2396_BASE, "g/", "http://a/b/c/g/")
+        self.checkJoin(RFC2396_BASE, "/g", "http://a/g")
+        self.checkJoin(RFC2396_BASE, "//g", "http://g")
+        self.checkJoin(RFC2396_BASE, "g?y", "http://a/b/c/g?y")
+        self.checkJoin(RFC2396_BASE, "#s", "http://a/b/c/d;p?q#s")
+        self.checkJoin(RFC2396_BASE, "g#s", "http://a/b/c/g#s")
+        self.checkJoin(RFC2396_BASE, "g?y#s", "http://a/b/c/g?y#s")
+        self.checkJoin(RFC2396_BASE, "g;x", "http://a/b/c/g;x")
+        self.checkJoin(RFC2396_BASE, "g;x?y#s", "http://a/b/c/g;x?y#s")
+        self.checkJoin(RFC2396_BASE, ".", "http://a/b/c/")
+        self.checkJoin(RFC2396_BASE, "./", "http://a/b/c/")
+        self.checkJoin(RFC2396_BASE, "..", "http://a/b/")
+        self.checkJoin(RFC2396_BASE, "../", "http://a/b/")
+        self.checkJoin(RFC2396_BASE, "../g", "http://a/b/g")
+        self.checkJoin(RFC2396_BASE, "../..", "http://a/")
+        self.checkJoin(RFC2396_BASE, "../../", "http://a/")
+        self.checkJoin(RFC2396_BASE, "../../g", "http://a/g")
+        self.checkJoin(RFC2396_BASE, "", RFC2396_BASE)
+        self.checkJoin(RFC2396_BASE, "../../../g", "http://a/../g")
+        self.checkJoin(RFC2396_BASE, "../../../../g", "http://a/../../g")
+        self.checkJoin(RFC2396_BASE, "/./g", "http://a/./g")
+        self.checkJoin(RFC2396_BASE, "/../g", "http://a/../g")
+        self.checkJoin(RFC2396_BASE, "g.", "http://a/b/c/g.")
+        self.checkJoin(RFC2396_BASE, ".g", "http://a/b/c/.g")
+        self.checkJoin(RFC2396_BASE, "g..", "http://a/b/c/g..")
+        self.checkJoin(RFC2396_BASE, "..g", "http://a/b/c/..g")
+        self.checkJoin(RFC2396_BASE, "./../g", "http://a/b/g")
+        self.checkJoin(RFC2396_BASE, "./g/.", "http://a/b/c/g/")
+        self.checkJoin(RFC2396_BASE, "g/./h", "http://a/b/c/g/h")
+        self.checkJoin(RFC2396_BASE, "g/../h", "http://a/b/c/h")
+        self.checkJoin(RFC2396_BASE, "g;x=1/./y", "http://a/b/c/g;x=1/y")
+        self.checkJoin(RFC2396_BASE, "g;x=1/../y", "http://a/b/c/y")
+        self.checkJoin(RFC2396_BASE, "g?y/./x", "http://a/b/c/g?y/./x")
+        self.checkJoin(RFC2396_BASE, "g?y/../x", "http://a/b/c/g?y/../x")
+        self.checkJoin(RFC2396_BASE, "g#s/./x", "http://a/b/c/g#s/./x")
+        self.checkJoin(RFC2396_BASE, "g#s/../x", "http://a/b/c/g#s/../x")
+
+    def test_RFC3986(self):
+        # Test cases from RFC3986
+        self.checkJoin(RFC3986_BASE, "?y", "http://a/b/c/d;p?y")
+        self.checkJoin(RFC2396_BASE, ";x", "http://a/b/c/;x")
+        self.checkJoin(RFC3986_BASE, "g:h", "g:h")
+        self.checkJoin(RFC3986_BASE, "g", "http://a/b/c/g")
+        self.checkJoin(RFC3986_BASE, "./g", "http://a/b/c/g")
+        self.checkJoin(RFC3986_BASE, "g/", "http://a/b/c/g/")
+        self.checkJoin(RFC3986_BASE, "/g", "http://a/g")
+        self.checkJoin(RFC3986_BASE, "//g", "http://g")
+        self.checkJoin(RFC3986_BASE, "?y", "http://a/b/c/d;p?y")
+        self.checkJoin(RFC3986_BASE, "g?y", "http://a/b/c/g?y")
+        self.checkJoin(RFC3986_BASE, "#s", "http://a/b/c/d;p?q#s")
+        self.checkJoin(RFC3986_BASE, "g#s", "http://a/b/c/g#s")
+        self.checkJoin(RFC3986_BASE, "g?y#s", "http://a/b/c/g?y#s")
+        self.checkJoin(RFC3986_BASE, ";x", "http://a/b/c/;x")
+        self.checkJoin(RFC3986_BASE, "g;x", "http://a/b/c/g;x")
+        self.checkJoin(RFC3986_BASE, "g;x?y#s", "http://a/b/c/g;x?y#s")
+        self.checkJoin(RFC3986_BASE, "", "http://a/b/c/d;p?q")
+        self.checkJoin(RFC3986_BASE, ".", "http://a/b/c/")
+        self.checkJoin(RFC3986_BASE, "./", "http://a/b/c/")
+        self.checkJoin(RFC3986_BASE, "..", "http://a/b/")
+        self.checkJoin(RFC3986_BASE, "../", "http://a/b/")
+        self.checkJoin(RFC3986_BASE, "../g", "http://a/b/g")
+        self.checkJoin(RFC3986_BASE, "../..", "http://a/")
+        self.checkJoin(RFC3986_BASE, "../../", "http://a/")
+        self.checkJoin(RFC3986_BASE, "../../g", "http://a/g")
+
+        # Abnormal Examples
+
+        # The 'abnormal scenarios' are incompatible with RFC2986 parsing
+        # Tests are here for reference.
+
+        # self.checkJoin(RFC3986_BASE, '../../../g','http://a/g')
+        # self.checkJoin(RFC3986_BASE, '../../../../g','http://a/g')
+        # self.checkJoin(RFC3986_BASE, '/./g','http://a/g')
+        # self.checkJoin(RFC3986_BASE, '/../g','http://a/g')
+
+        self.checkJoin(RFC3986_BASE, "g.", "http://a/b/c/g.")
+        self.checkJoin(RFC3986_BASE, ".g", "http://a/b/c/.g")
+        self.checkJoin(RFC3986_BASE, "g..", "http://a/b/c/g..")
+        self.checkJoin(RFC3986_BASE, "..g", "http://a/b/c/..g")
+        self.checkJoin(RFC3986_BASE, "./../g", "http://a/b/g")
+        self.checkJoin(RFC3986_BASE, "./g/.", "http://a/b/c/g/")
+        self.checkJoin(RFC3986_BASE, "g/./h", "http://a/b/c/g/h")
+        self.checkJoin(RFC3986_BASE, "g/../h", "http://a/b/c/h")
+        self.checkJoin(RFC3986_BASE, "g;x=1/./y", "http://a/b/c/g;x=1/y")
+        self.checkJoin(RFC3986_BASE, "g;x=1/../y", "http://a/b/c/y")
+        self.checkJoin(RFC3986_BASE, "g?y/./x", "http://a/b/c/g?y/./x")
+        self.checkJoin(RFC3986_BASE, "g?y/../x", "http://a/b/c/g?y/../x")
+        self.checkJoin(RFC3986_BASE, "g#s/./x", "http://a/b/c/g#s/./x")
+        self.checkJoin(RFC3986_BASE, "g#s/../x", "http://a/b/c/g#s/../x")
+        # self.checkJoin(RFC3986_BASE, 'http:g','http:g') # strict parser
+        self.checkJoin(RFC3986_BASE, "http:g", "http://a/b/c/g")  # relaxed parser
+
+        # Test for issue9721
+        self.checkJoin("http://a/b/c/de", ";x", "http://a/b/c/;x")
+
+    def test_urljoins(self):
+        self.checkJoin(SIMPLE_BASE, "g:h", "g:h")
+        self.checkJoin(SIMPLE_BASE, "http:g", "http://a/b/c/g")
+        self.checkJoin(SIMPLE_BASE, "http:", "http://a/b/c/d")
+        self.checkJoin(SIMPLE_BASE, "g", "http://a/b/c/g")
+        self.checkJoin(SIMPLE_BASE, "./g", "http://a/b/c/g")
+        self.checkJoin(SIMPLE_BASE, "g/", "http://a/b/c/g/")
+        self.checkJoin(SIMPLE_BASE, "/g", "http://a/g")
+        self.checkJoin(SIMPLE_BASE, "//g", "http://g")
+        self.checkJoin(SIMPLE_BASE, "?y", "http://a/b/c/d?y")
+        self.checkJoin(SIMPLE_BASE, "g?y", "http://a/b/c/g?y")
+        self.checkJoin(SIMPLE_BASE, "g?y/./x", "http://a/b/c/g?y/./x")
+        self.checkJoin(SIMPLE_BASE, ".", "http://a/b/c/")
+        self.checkJoin(SIMPLE_BASE, "./", "http://a/b/c/")
+        self.checkJoin(SIMPLE_BASE, "..", "http://a/b/")
+        self.checkJoin(SIMPLE_BASE, "../", "http://a/b/")
+        self.checkJoin(SIMPLE_BASE, "../g", "http://a/b/g")
+        self.checkJoin(SIMPLE_BASE, "../..", "http://a/")
+        self.checkJoin(SIMPLE_BASE, "../../g", "http://a/g")
+        self.checkJoin(SIMPLE_BASE, "../../../g", "http://a/../g")
+        self.checkJoin(SIMPLE_BASE, "./../g", "http://a/b/g")
+        self.checkJoin(SIMPLE_BASE, "./g/.", "http://a/b/c/g/")
+        self.checkJoin(SIMPLE_BASE, "/./g", "http://a/./g")
+        self.checkJoin(SIMPLE_BASE, "g/./h", "http://a/b/c/g/h")
+        self.checkJoin(SIMPLE_BASE, "g/../h", "http://a/b/c/h")
+        self.checkJoin(SIMPLE_BASE, "http:g", "http://a/b/c/g")
+        self.checkJoin(SIMPLE_BASE, "http:", "http://a/b/c/d")
+        self.checkJoin(SIMPLE_BASE, "http:?y", "http://a/b/c/d?y")
+        self.checkJoin(SIMPLE_BASE, "http:g?y", "http://a/b/c/g?y")
+        self.checkJoin(SIMPLE_BASE, "http:g?y/./x", "http://a/b/c/g?y/./x")
+        self.checkJoin("http:///", "..", "http:///")
+        self.checkJoin("", "http://a/b/c/g?y/./x", "http://a/b/c/g?y/./x")
+        self.checkJoin("", "http://a/./g", "http://a/./g")
+        self.checkJoin("svn://pathtorepo/dir1", "dir2", "svn://pathtorepo/dir2")
+        self.checkJoin("svn+ssh://pathtorepo/dir1", "dir2", "svn+ssh://pathtorepo/dir2")
+
+    def test_RFC2732(self):
+        str_cases = [
+            ("http://Test.python.org:5432/foo/", "test.python.org", 5432),
+            ("http://12.34.56.78:5432/foo/", "12.34.56.78", 5432),
+            ("http://[::1]:5432/foo/", "::1", 5432),
+            ("http://[dead:beef::1]:5432/foo/", "dead:beef::1", 5432),
+            ("http://[dead:beef::]:5432/foo/", "dead:beef::", 5432),
+            (
+                "http://[dead:beef:cafe:5417:affe:8FA3:deaf:feed]:5432/foo/",
+                "dead:beef:cafe:5417:affe:8fa3:deaf:feed",
+                5432,
+            ),
+            ("http://[::12.34.56.78]:5432/foo/", "::12.34.56.78", 5432),
+            ("http://[::ffff:12.34.56.78]:5432/foo/", "::ffff:12.34.56.78", 5432),
+            ("http://Test.python.org/foo/", "test.python.org", None),
+            ("http://12.34.56.78/foo/", "12.34.56.78", None),
+            ("http://[::1]/foo/", "::1", None),
+            ("http://[dead:beef::1]/foo/", "dead:beef::1", None),
+            ("http://[dead:beef::]/foo/", "dead:beef::", None),
+            (
+                "http://[dead:beef:cafe:5417:affe:8FA3:deaf:feed]/foo/",
+                "dead:beef:cafe:5417:affe:8fa3:deaf:feed",
+                None,
+            ),
+            ("http://[::12.34.56.78]/foo/", "::12.34.56.78", None),
+            ("http://[::ffff:12.34.56.78]/foo/", "::ffff:12.34.56.78", None),
+        ]
+
+        def _encode(t):
+            return t[0].encode("ascii"), t[1].encode("ascii"), t[2]
+
+        bytes_cases = [_encode(x) for x in str_cases]
+        for url, hostname, port in str_cases + bytes_cases:
+            urlparsed = urllib.parse.urlparse(url)
+            self.assertEqual((urlparsed.hostname, urlparsed.port), (hostname, port))
+
+        str_cases = [
+            "http://::12.34.56.78]/",
+            "http://[::1/foo/",
+            "ftp://[::1/foo/bad]/bad",
+            "http://[::1/foo/bad]/bad",
+            "http://[::ffff:12.34.56.78",
+        ]
+        bytes_cases = [x.encode("ascii") for x in str_cases]
+        for invalid_url in str_cases + bytes_cases:
+            self.assertRaises(ValueError, urllib.parse.urlparse, invalid_url)
+
+    def test_urldefrag(self):
+        str_cases = [
+            ("http://python.org#frag", "http://python.org", "frag"),
+            ("http://python.org", "http://python.org", ""),
+            ("http://python.org/#frag", "http://python.org/", "frag"),
+            ("http://python.org/", "http://python.org/", ""),
+            ("http://python.org/?q#frag", "http://python.org/?q", "frag"),
+            ("http://python.org/?q", "http://python.org/?q", ""),
+            ("http://python.org/p#frag", "http://python.org/p", "frag"),
+            ("http://python.org/p?q", "http://python.org/p?q", ""),
+            (RFC1808_BASE, "http://a/b/c/d;p?q", "f"),
+            (RFC2396_BASE, "http://a/b/c/d;p?q", ""),
+        ]
+
+        def _encode(t):
+            return type(t)(x.encode("ascii") for x in t)
+
+        bytes_cases = [_encode(x) for x in str_cases]
+        for url, defrag, frag in str_cases + bytes_cases:
+            result = urllib.parse.urldefrag(url)
+            self.assertEqual(result.geturl(), url)
+            self.assertEqual(result, (defrag, frag))
+            self.assertEqual(result.url, defrag)
+            self.assertEqual(result.fragment, frag)
+
+    def test_urlsplit_attributes(self):
+        url = "HTTP://WWW.PYTHON.ORG/doc/#frag"
+        p = urllib.parse.urlsplit(url)
+        self.assertEqual(p.scheme, "http")
+        self.assertEqual(p.netloc, "WWW.PYTHON.ORG")
+        self.assertEqual(p.path, "/doc/")
+        self.assertEqual(p.query, "")
+        self.assertEqual(p.fragment, "frag")
+        self.assertEqual(p.username, None)
+        self.assertEqual(p.password, None)
+        self.assertEqual(p.hostname, "www.python.org")
+        self.assertEqual(p.port, None)
+        # geturl() won't return exactly the original URL in this case
+        # since the scheme is always case-normalized
+        # We handle this by ignoring the first 4 characters of the URL
+        self.assertEqual(p.geturl()[4:], url[4:])
+
+        url = "http://User:Pass@www.python.org:080/doc/?query=yes#frag"
+        p = urllib.parse.urlsplit(url)
+        self.assertEqual(p.scheme, "http")
+        self.assertEqual(p.netloc, "User:Pass@www.python.org:080")
+        self.assertEqual(p.path, "/doc/")
+        self.assertEqual(p.query, "query=yes")
+        self.assertEqual(p.fragment, "frag")
+        self.assertEqual(p.username, "User")
+        self.assertEqual(p.password, "Pass")
+        self.assertEqual(p.hostname, "www.python.org")
+        self.assertEqual(p.port, 80)
+        self.assertEqual(p.geturl(), url)
+
+        # Addressing issue1698, which suggests Username can contain
+        # "@" characters.  Though not RFC compliant, many ftp sites allow
+        # and request email addresses as usernames.
+
+        url = "http://User@example.com:Pass@www.python.org:080/doc/?query=yes#frag"
+        p = urllib.parse.urlsplit(url)
+        self.assertEqual(p.scheme, "http")
+        self.assertEqual(p.netloc, "User@example.com:Pass@www.python.org:080")
+        self.assertEqual(p.path, "/doc/")
+        self.assertEqual(p.query, "query=yes")
+        self.assertEqual(p.fragment, "frag")
+        self.assertEqual(p.username, "User@example.com")
+        self.assertEqual(p.password, "Pass")
+        self.assertEqual(p.hostname, "www.python.org")
+        self.assertEqual(p.port, 80)
+        self.assertEqual(p.geturl(), url)
+
+        # And check them all again, only with bytes this time
+        url = b"HTTP://WWW.PYTHON.ORG/doc/#frag"
+        p = urllib.parse.urlsplit(url)
+        self.assertEqual(p.scheme, b"http")
+        self.assertEqual(p.netloc, b"WWW.PYTHON.ORG")
+        self.assertEqual(p.path, b"/doc/")
+        self.assertEqual(p.query, b"")
+        self.assertEqual(p.fragment, b"frag")
+        self.assertEqual(p.username, None)
+        self.assertEqual(p.password, None)
+        self.assertEqual(p.hostname, b"www.python.org")
+        self.assertEqual(p.port, None)
+        self.assertEqual(p.geturl()[4:], url[4:])
+
+        url = b"http://User:Pass@www.python.org:080/doc/?query=yes#frag"
+        p = urllib.parse.urlsplit(url)
+        self.assertEqual(p.scheme, b"http")
+        self.assertEqual(p.netloc, b"User:Pass@www.python.org:080")
+        self.assertEqual(p.path, b"/doc/")
+        self.assertEqual(p.query, b"query=yes")
+        self.assertEqual(p.fragment, b"frag")
+        self.assertEqual(p.username, b"User")
+        self.assertEqual(p.password, b"Pass")
+        self.assertEqual(p.hostname, b"www.python.org")
+        self.assertEqual(p.port, 80)
+        self.assertEqual(p.geturl(), url)
+
+        url = b"http://User@example.com:Pass@www.python.org:080/doc/?query=yes#frag"
+        p = urllib.parse.urlsplit(url)
+        self.assertEqual(p.scheme, b"http")
+        self.assertEqual(p.netloc, b"User@example.com:Pass@www.python.org:080")
+        self.assertEqual(p.path, b"/doc/")
+        self.assertEqual(p.query, b"query=yes")
+        self.assertEqual(p.fragment, b"frag")
+        self.assertEqual(p.username, b"User@example.com")
+        self.assertEqual(p.password, b"Pass")
+        self.assertEqual(p.hostname, b"www.python.org")
+        self.assertEqual(p.port, 80)
+        self.assertEqual(p.geturl(), url)
+
+        # Verify an illegal port is returned as None
+        url = b"HTTP://WWW.PYTHON.ORG:65536/doc/#frag"
+        p = urllib.parse.urlsplit(url)
+        self.assertEqual(p.port, None)
+
+    def test_attributes_bad_port(self):
+        """Check handling of non-integer ports."""
+        p = urllib.parse.urlsplit("http://www.example.net:foo")
+        self.assertEqual(p.netloc, "www.example.net:foo")
+        self.assertRaises(ValueError, lambda: p.port)
+
+        p = urllib.parse.urlparse("http://www.example.net:foo")
+        self.assertEqual(p.netloc, "www.example.net:foo")
+        self.assertRaises(ValueError, lambda: p.port)
+
+        # Once again, repeat ourselves to test bytes
+        p = urllib.parse.urlsplit(b"http://www.example.net:foo")
+        self.assertEqual(p.netloc, b"www.example.net:foo")
+        self.assertRaises(ValueError, lambda: p.port)
+
+        p = urllib.parse.urlparse(b"http://www.example.net:foo")
+        self.assertEqual(p.netloc, b"www.example.net:foo")
+        self.assertRaises(ValueError, lambda: p.port)
+
+    def test_attributes_without_netloc(self):
+        # This example is straight from RFC 3261.  It looks like it
+        # should allow the username, hostname, and port to be filled
+        # in, but doesn't.  Since it's a URI and doesn't use the
+        # scheme://netloc syntax, the netloc and related attributes
+        # should be left empty.
+        uri = "sip:alice@atlanta.com;maddr=239.255.255.1;ttl=15"
+        p = urllib.parse.urlsplit(uri)
+        self.assertEqual(p.netloc, "")
+        self.assertEqual(p.username, None)
+        self.assertEqual(p.password, None)
+        self.assertEqual(p.hostname, None)
+        self.assertEqual(p.port, None)
+        self.assertEqual(p.geturl(), uri)
+
+        p = urllib.parse.urlparse(uri)
+        self.assertEqual(p.netloc, "")
+        self.assertEqual(p.username, None)
+        self.assertEqual(p.password, None)
+        self.assertEqual(p.hostname, None)
+        self.assertEqual(p.port, None)
+        self.assertEqual(p.geturl(), uri)
+
+        # You guessed it, repeating the test with bytes input
+        uri = b"sip:alice@atlanta.com;maddr=239.255.255.1;ttl=15"
+        p = urllib.parse.urlsplit(uri)
+        self.assertEqual(p.netloc, b"")
+        self.assertEqual(p.username, None)
+        self.assertEqual(p.password, None)
+        self.assertEqual(p.hostname, None)
+        self.assertEqual(p.port, None)
+        self.assertEqual(p.geturl(), uri)
+
+        p = urllib.parse.urlparse(uri)
+        self.assertEqual(p.netloc, b"")
+        self.assertEqual(p.username, None)
+        self.assertEqual(p.password, None)
+        self.assertEqual(p.hostname, None)
+        self.assertEqual(p.port, None)
+        self.assertEqual(p.geturl(), uri)
+
+    def test_noslash(self):
+        # Issue 1637: http://foo.com?query is legal
+        self.assertEqual(
+            urllib.parse.urlparse("http://example.com?blahblah=/foo"),
+            ("http", "example.com", "", "", "blahblah=/foo", ""),
+        )
+        self.assertEqual(
+            urllib.parse.urlparse(b"http://example.com?blahblah=/foo"),
+            (b"http", b"example.com", b"", b"", b"blahblah=/foo", b""),
+        )
+
+    def test_withoutscheme(self):
+        # Test urlparse without scheme
+        # Issue 754016: urlparse goes wrong with IP:port without scheme
+        # RFC 1808 specifies that netloc should start with //, urlparse expects
+        # the same, otherwise it classifies the portion of url as path.
+        self.assertEqual(urllib.parse.urlparse("path"), ("", "", "path", "", "", ""))
+        self.assertEqual(
+            urllib.parse.urlparse("//www.python.org:80"), ("", "www.python.org:80", "", "", "", "")
+        )
+        self.assertEqual(
+            urllib.parse.urlparse("http://www.python.org:80"),
+            ("http", "www.python.org:80", "", "", "", ""),
+        )
+        # Repeat for bytes input
+        self.assertEqual(urllib.parse.urlparse(b"path"), (b"", b"", b"path", b"", b"", b""))
+        self.assertEqual(
+            urllib.parse.urlparse(b"//www.python.org:80"),
+            (b"", b"www.python.org:80", b"", b"", b"", b""),
+        )
+        self.assertEqual(
+            urllib.parse.urlparse(b"http://www.python.org:80"),
+            (b"http", b"www.python.org:80", b"", b"", b"", b""),
+        )
+
+    def test_portseparator(self):
+        # Issue 754016 makes changes for port separator ':' from scheme separator
+        self.assertEqual(urllib.parse.urlparse("path:80"), ("", "", "path:80", "", "", ""))
+        self.assertEqual(urllib.parse.urlparse("http:"), ("http", "", "", "", "", ""))
+        self.assertEqual(urllib.parse.urlparse("https:"), ("https", "", "", "", "", ""))
+        self.assertEqual(
+            urllib.parse.urlparse("http://www.python.org:80"),
+            ("http", "www.python.org:80", "", "", "", ""),
+        )
+        # As usual, need to check bytes input as well
+        self.assertEqual(urllib.parse.urlparse(b"path:80"), (b"", b"", b"path:80", b"", b"", b""))
+        self.assertEqual(urllib.parse.urlparse(b"http:"), (b"http", b"", b"", b"", b"", b""))
+        self.assertEqual(urllib.parse.urlparse(b"https:"), (b"https", b"", b"", b"", b"", b""))
+        self.assertEqual(
+            urllib.parse.urlparse(b"http://www.python.org:80"),
+            (b"http", b"www.python.org:80", b"", b"", b"", b""),
+        )
+
+    def test_usingsys(self):
+        # Issue 3314: sys module is used in the error
+        self.assertRaises(TypeError, urllib.parse.urlencode, "foo")
+
+    def test_anyscheme(self):
+        # Issue 7904: s3://foo.com/stuff has netloc "foo.com".
+        self.assertEqual(
+            urllib.parse.urlparse("s3://foo.com/stuff"), ("s3", "foo.com", "/stuff", "", "", "")
+        )
+        self.assertEqual(
+            urllib.parse.urlparse("x-newscheme://foo.com/stuff"),
+            ("x-newscheme", "foo.com", "/stuff", "", "", ""),
+        )
+        self.assertEqual(
+            urllib.parse.urlparse("x-newscheme://foo.com/stuff?query#fragment"),
+            ("x-newscheme", "foo.com", "/stuff", "", "query", "fragment"),
+        )
+        self.assertEqual(
+            urllib.parse.urlparse("x-newscheme://foo.com/stuff?query"),
+            ("x-newscheme", "foo.com", "/stuff", "", "query", ""),
+        )
+
+        # And for bytes...
+        self.assertEqual(
+            urllib.parse.urlparse(b"s3://foo.com/stuff"),
+            (b"s3", b"foo.com", b"/stuff", b"", b"", b""),
+        )
+        self.assertEqual(
+            urllib.parse.urlparse(b"x-newscheme://foo.com/stuff"),
+            (b"x-newscheme", b"foo.com", b"/stuff", b"", b"", b""),
+        )
+        self.assertEqual(
+            urllib.parse.urlparse(b"x-newscheme://foo.com/stuff?query#fragment"),
+            (b"x-newscheme", b"foo.com", b"/stuff", b"", b"query", b"fragment"),
+        )
+        self.assertEqual(
+            urllib.parse.urlparse(b"x-newscheme://foo.com/stuff?query"),
+            (b"x-newscheme", b"foo.com", b"/stuff", b"", b"query", b""),
+        )
+
+    def _test_mixed_types_rejected(self):
+        # Several functions that process either strings or ASCII encoded bytes
+        # accept multiple arguments. Check they reject mixed type input
+        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
+            urllib.parse.urlparse("www.python.org", b"http")
+        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
+            urllib.parse.urlparse(b"www.python.org", "http")
+        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
+            urllib.parse.urlsplit("www.python.org", b"http")
+        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
+            urllib.parse.urlsplit(b"www.python.org", "http")
+        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
+            urllib.parse.urlunparse((b"http", "www.python.org", "", "", "", ""))
+        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
+            urllib.parse.urlunparse(("http", b"www.python.org", "", "", "", ""))
+        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
+            urllib.parse.urlunsplit((b"http", "www.python.org", "", "", ""))
+        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
+            urllib.parse.urlunsplit(("http", b"www.python.org", "", "", ""))
+        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
+            urllib.parse.urljoin("http://python.org", b"http://python.org")
+        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
+            urllib.parse.urljoin(b"http://python.org", "http://python.org")
+
+    def _check_result_type(self, str_type, num_args):
+        #        num_args = len(str_type._fields)
+        bytes_type = str_type._encoded_counterpart
+        self.assertIs(bytes_type._decoded_counterpart, str_type)
+        str_args = ("",) * num_args
+        bytes_args = (b"",) * num_args
+        str_result = str_type(*str_args)
+        bytes_result = bytes_type(*bytes_args)
+        encoding = "ascii"
+        errors = "strict"
+        self.assertEqual(str_result, str_args)
+        self.assertEqual(bytes_result.decode(), str_args)
+        self.assertEqual(bytes_result.decode(), str_result)
+        self.assertEqual(bytes_result.decode(encoding), str_args)
+        self.assertEqual(bytes_result.decode(encoding), str_result)
+        self.assertEqual(bytes_result.decode(encoding, errors), str_args)
+        self.assertEqual(bytes_result.decode(encoding, errors), str_result)
+        self.assertEqual(bytes_result, bytes_args)
+        self.assertEqual(str_result.encode(), bytes_args)
+        self.assertEqual(str_result.encode(), bytes_result)
+        self.assertEqual(str_result.encode(encoding), bytes_args)
+        self.assertEqual(str_result.encode(encoding), bytes_result)
+        self.assertEqual(str_result.encode(encoding, errors), bytes_args)
+        self.assertEqual(str_result.encode(encoding, errors), bytes_result)
+
+    def test_result_pairs(self):
+        # Check encoding and decoding between result pairs
+        result_types = [
+            (urllib.parse.DefragResult, 2),
+            (urllib.parse.SplitResult, 5),
+            (urllib.parse.ParseResult, 6),
+        ]
+        for result_type in result_types:
+            self._check_result_type(*result_type)
+
+    def _test_parse_qs_encoding(self):
+        result = urllib.parse.parse_qs("key=\u0141%E9", encoding="latin-1")
+        self.assertEqual(result, {"key": ["\u0141\xE9"]})
+        result = urllib.parse.parse_qs("key=\u0141%C3%A9", encoding="utf-8")
+        self.assertEqual(result, {"key": ["\u0141\xE9"]})
+        result = urllib.parse.parse_qs("key=\u0141%C3%A9", encoding="ascii")
+        self.assertEqual(result, {"key": ["\u0141\ufffd\ufffd"]})
+        result = urllib.parse.parse_qs("key=\u0141%E9-", encoding="ascii")
+        self.assertEqual(result, {"key": ["\u0141\ufffd-"]})
+        result = urllib.parse.parse_qs("key=\u0141%E9-", encoding="ascii", errors="ignore")
+        self.assertEqual(result, {"key": ["\u0141-"]})
+
+    def _test_parse_qsl_encoding(self):
+        result = urllib.parse.parse_qsl("key=\u0141%E9", encoding="latin-1")
+        self.assertEqual(result, [("key", "\u0141\xE9")])
+        result = urllib.parse.parse_qsl("key=\u0141%C3%A9", encoding="utf-8")
+        self.assertEqual(result, [("key", "\u0141\xE9")])
+        result = urllib.parse.parse_qsl("key=\u0141%C3%A9", encoding="ascii")
+        self.assertEqual(result, [("key", "\u0141\ufffd\ufffd")])
+        result = urllib.parse.parse_qsl("key=\u0141%E9-", encoding="ascii")
+        self.assertEqual(result, [("key", "\u0141\ufffd-")])
+        result = urllib.parse.parse_qsl("key=\u0141%E9-", encoding="ascii", errors="ignore")
+        self.assertEqual(result, [("key", "\u0141-")])
+
+    def test_splitnport(self):
+        # Normal cases are exercised by other tests; ensure that we also
+        # catch cases with no port specified. (testcase ensuring coverage)
+        result = urllib.parse.splitnport("parrot:88")
+        self.assertEqual(result, ("parrot", 88))
+        result = urllib.parse.splitnport("parrot")
+        self.assertEqual(result, ("parrot", -1))
+        result = urllib.parse.splitnport("parrot", 55)
+        self.assertEqual(result, ("parrot", 55))
+        result = urllib.parse.splitnport("parrot:")
+        self.assertEqual(result, ("parrot", None))
+
+    def test_splitquery(self):
+        # Normal cases are exercised by other tests; ensure that we also
+        # catch cases with no port specified (testcase ensuring coverage)
+        result = urllib.parse.splitquery("http://python.org/fake?foo=bar")
+        self.assertEqual(result, ("http://python.org/fake", "foo=bar"))
+        result = urllib.parse.splitquery("http://python.org/fake?foo=bar?")
+        self.assertEqual(result, ("http://python.org/fake?foo=bar", ""))
+        result = urllib.parse.splitquery("http://python.org/fake")
+        self.assertEqual(result, ("http://python.org/fake", None))
+
+    def test_splitvalue(self):
+        # Normal cases are exercised by other tests; test pathological cases
+        # with no key/value pairs. (testcase ensuring coverage)
+        result = urllib.parse.splitvalue("foo=bar")
+        self.assertEqual(result, ("foo", "bar"))
+        result = urllib.parse.splitvalue("foo=")
+        self.assertEqual(result, ("foo", ""))
+        result = urllib.parse.splitvalue("foobar")
+        self.assertEqual(result, ("foobar", None))
+
+    def test_to_bytes(self):
+        result = urllib.parse.to_bytes("http://www.python.org")
+        self.assertEqual(result, "http://www.python.org")
+
+    #        self.assertRaises(UnicodeError, urllib.parse.to_bytes,
+    #                          'http://www.python.org/medi\u00e6val')
+
+    def test_urlencode_sequences(self):
+        # Other tests incidentally urlencode things; test non-covered cases:
+        # Sequence and object values.
+        result = urllib.parse.urlencode({"a": [1, 2], "b": (3, 4, 5)}, True)
+        # we cannot rely on ordering here
+        assert set(result.split("&")) == {"a=1", "a=2", "b=3", "b=4", "b=5"}
+
+        class Trivial:
+            def __str__(self):
+                return "trivial"
+
+        result = urllib.parse.urlencode({"a": Trivial()}, True)
+        self.assertEqual(result, "a=trivial")
+
+    def test_quote_from_bytes(self):
+        self.assertRaises(TypeError, urllib.parse.quote_from_bytes, "foo")
+        result = urllib.parse.quote_from_bytes(b"archaeological arcana")
+        self.assertEqual(result, "archaeological%20arcana")
+        result = urllib.parse.quote_from_bytes(b"")
+        self.assertEqual(result, "")
+
+    def test_unquote_to_bytes(self):
+        result = urllib.parse.unquote_to_bytes("abc%20def")
+        self.assertEqual(result, b"abc def")
+        result = urllib.parse.unquote_to_bytes("")
+        self.assertEqual(result, b"")
+
+    def test_quote_errors(self):
+        self.assertRaises(TypeError, urllib.parse.quote, b"foo", encoding="utf-8")
+        self.assertRaises(TypeError, urllib.parse.quote, b"foo", errors="strict")
+
+    def test_issue14072(self):
+        p1 = urllib.parse.urlsplit("tel:+31-641044153")
+        self.assertEqual(p1.scheme, "tel")
+        self.assertEqual(p1.path, "+31-641044153")
+        p2 = urllib.parse.urlsplit("tel:+31641044153")
+        self.assertEqual(p2.scheme, "tel")
+        self.assertEqual(p2.path, "+31641044153")
+        # assert the behavior for urlparse
+        p1 = urllib.parse.urlparse("tel:+31-641044153")
+        self.assertEqual(p1.scheme, "tel")
+        self.assertEqual(p1.path, "+31-641044153")
+        p2 = urllib.parse.urlparse("tel:+31641044153")
+        self.assertEqual(p2.scheme, "tel")
+        self.assertEqual(p2.path, "+31641044153")
+
+    def test_telurl_params(self):
+        p1 = urllib.parse.urlparse("tel:123-4;phone-context=+1-650-516")
+        self.assertEqual(p1.scheme, "tel")
+        self.assertEqual(p1.path, "123-4")
+        self.assertEqual(p1.params, "phone-context=+1-650-516")
+
+        p1 = urllib.parse.urlparse("tel:+1-201-555-0123")
+        self.assertEqual(p1.scheme, "tel")
+        self.assertEqual(p1.path, "+1-201-555-0123")
+        self.assertEqual(p1.params, "")
+
+        p1 = urllib.parse.urlparse("tel:7042;phone-context=example.com")
+        self.assertEqual(p1.scheme, "tel")
+        self.assertEqual(p1.path, "7042")
+        self.assertEqual(p1.params, "phone-context=example.com")
+
+        p1 = urllib.parse.urlparse("tel:863-1234;phone-context=+1-914-555")
+        self.assertEqual(p1.scheme, "tel")
+        self.assertEqual(p1.path, "863-1234")
+        self.assertEqual(p1.params, "phone-context=+1-914-555")
+
+
+def test_main():
+    support.run_unittest(UrlParseTestCase)
+
+
+if __name__ == "__main__":
+    test_main()
diff --git a/unix-ffi/urllib.parse/urllib/parse.py b/unix-ffi/urllib.parse/urllib/parse.py
new file mode 100644
index 000000000..1ec75378c
--- /dev/null
+++ b/unix-ffi/urllib.parse/urllib/parse.py
@@ -0,0 +1,1151 @@
+"""Parse (absolute and relative) URLs.
+
+urlparse module is based upon the following RFC specifications.
+
+RFC 3986 (STD66): "Uniform Resource Identifiers" by T. Berners-Lee, R. Fielding
+and L.  Masinter, January 2005.
+
+RFC 2732 : "Format for Literal IPv6 Addresses in URL's by R.Hinden, B.Carpenter
+and L.Masinter, December 1999.
+
+RFC 2396:  "Uniform Resource Identifiers (URI)": Generic Syntax by T.
+Berners-Lee, R. Fielding, and L. Masinter, August 1998.
+
+RFC 2368: "The mailto URL scheme", by P.Hoffman , L Masinter, J. Zawinski, July 1998.
+
+RFC 1808: "Relative Uniform Resource Locators", by R. Fielding, UC Irvine, June
+1995.
+
+RFC 1738: "Uniform Resource Locators (URL)" by T. Berners-Lee, L. Masinter, M.
+McCahill, December 1994
+
+RFC 3986 is considered the current standard and any future changes to
+urlparse module should conform with it.  The urlparse module is
+currently not entirely compliant with this RFC due to defacto
+scenarios for parsing, and for backward compatibility purposes, some
+parsing quirks from older RFCs are retained. The testcases in
+test_urlparse.py provides a good indicator of parsing behavior.
+"""
+
+import re
+import sys
+import collections
+
+__all__ = [
+    "urlparse",
+    "urlunparse",
+    "urljoin",
+    "urldefrag",
+    "urlsplit",
+    "urlunsplit",
+    "urlencode",
+    "parse_qs",
+    "parse_qsl",
+    "quote",
+    "quote_plus",
+    "quote_from_bytes",
+    "unquote",
+    "unquote_plus",
+    "unquote_to_bytes",
+]
+
+# A classification of schemes ('' means apply by default)
+uses_relative = [
+    "ftp",
+    "http",
+    "gopher",
+    "nntp",
+    "imap",
+    "wais",
+    "file",
+    "https",
+    "shttp",
+    "mms",
+    "prospero",
+    "rtsp",
+    "rtspu",
+    "",
+    "sftp",
+    "svn",
+    "svn+ssh",
+]
+uses_netloc = [
+    "ftp",
+    "http",
+    "gopher",
+    "nntp",
+    "telnet",
+    "imap",
+    "wais",
+    "file",
+    "mms",
+    "https",
+    "shttp",
+    "snews",
+    "prospero",
+    "rtsp",
+    "rtspu",
+    "rsync",
+    "",
+    "svn",
+    "svn+ssh",
+    "sftp",
+    "nfs",
+    "git",
+    "git+ssh",
+]
+uses_params = [
+    "ftp",
+    "hdl",
+    "prospero",
+    "http",
+    "imap",
+    "https",
+    "shttp",
+    "rtsp",
+    "rtspu",
+    "sip",
+    "sips",
+    "mms",
+    "",
+    "sftp",
+    "tel",
+]
+
+# These are not actually used anymore, but should stay for backwards
+# compatibility.  (They are undocumented, but have a public-looking name.)
+non_hierarchical = [
+    "gopher",
+    "hdl",
+    "mailto",
+    "news",
+    "telnet",
+    "wais",
+    "imap",
+    "snews",
+    "sip",
+    "sips",
+]
+uses_query = [
+    "http",
+    "wais",
+    "imap",
+    "https",
+    "shttp",
+    "mms",
+    "gopher",
+    "rtsp",
+    "rtspu",
+    "sip",
+    "sips",
+    "",
+]
+uses_fragment = [
+    "ftp",
+    "hdl",
+    "http",
+    "gopher",
+    "news",
+    "nntp",
+    "wais",
+    "https",
+    "shttp",
+    "snews",
+    "file",
+    "prospero",
+    "",
+]
+
+# Characters valid in scheme names
+scheme_chars = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" "+-."
+
+# XXX: Consider replacing with functools.lru_cache
+MAX_CACHE_SIZE = 20
+_parse_cache = {}
+
+
+def clear_cache():
+    """Clear the parse cache and the quoters cache."""
+    _parse_cache.clear()
+    _safe_quoters.clear()
+
+
+# Helpers for bytes handling
+# For 3.2, we deliberately require applications that
+# handle improperly quoted URLs to do their own
+# decoding and encoding. If valid use cases are
+# presented, we may relax this by using latin-1
+# decoding internally for 3.3
+_implicit_encoding = "ascii"
+_implicit_errors = "strict"
+
+
+def _noop(obj):
+    return obj
+
+
+def _encode_result(obj, encoding=_implicit_encoding, errors=_implicit_errors):
+    return obj.encode(encoding, errors)
+
+
+def _decode_args(args, encoding=_implicit_encoding, errors=_implicit_errors):
+    return tuple(x.decode(encoding, errors) if x else "" for x in args)
+
+
+def _coerce_args(*args):
+    # Invokes decode if necessary to create str args
+    # and returns the coerced inputs along with
+    # an appropriate result coercion function
+    #   - noop for str inputs
+    #   - encoding function otherwise
+    str_input = isinstance(args[0], str)
+    for arg in args[1:]:
+        # We special-case the empty string to support the
+        # "scheme=''" default argument to some functions
+        if arg and isinstance(arg, str) != str_input:
+            raise TypeError("Cannot mix str and non-str arguments")
+    if str_input:
+        return args + (_noop,)
+    return _decode_args(args) + (_encode_result,)
+
+
+# Result objects are more helpful than simple tuples
+class _ResultMixinStr(object):
+    """Standard approach to encoding parsed results from str to bytes"""
+
+    __slots__ = ()
+
+    def encode(self, encoding="ascii", errors="strict"):
+        return self._encoded_counterpart(*(x.encode(encoding, errors) for x in self))
+
+
+class _ResultMixinBytes(object):
+    """Standard approach to decoding parsed results from bytes to str"""
+
+    __slots__ = ()
+
+    def decode(self, encoding="ascii", errors="strict"):
+        return self._decoded_counterpart(*(x.decode(encoding, errors) for x in self))
+
+
+class _NetlocResultMixinBase(object):
+    """Shared methods for the parsed result objects containing a netloc element"""
+
+    __slots__ = ()
+
+    @property
+    def username(self):
+        return self._userinfo[0]
+
+    @property
+    def password(self):
+        return self._userinfo[1]
+
+    @property
+    def hostname(self):
+        hostname = self._hostinfo[0]
+        if not hostname:
+            hostname = None
+        elif hostname is not None:
+            hostname = hostname.lower()
+        return hostname
+
+    @property
+    def port(self):
+        port = self._hostinfo[1]
+        if port is not None:
+            port = int(port, 10)
+            # Return None on an illegal port
+            if not (0 <= port <= 65535):
+                return None
+        return port
+
+
+class _NetlocResultMixinStr(_NetlocResultMixinBase, _ResultMixinStr):
+    __slots__ = ()
+
+    @property
+    def _userinfo(self):
+        netloc = self.netloc
+        userinfo, have_info, hostinfo = netloc.rpartition("@")
+        if have_info:
+            username, have_password, password = userinfo.partition(":")
+            if not have_password:
+                password = None
+        else:
+            username = password = None
+        return username, password
+
+    @property
+    def _hostinfo(self):
+        netloc = self.netloc
+        _, _, hostinfo = netloc.rpartition("@")
+        _, have_open_br, bracketed = hostinfo.partition("[")
+        if have_open_br:
+            hostname, _, port = bracketed.partition("]")
+            _, have_port, port = port.partition(":")
+        else:
+            hostname, have_port, port = hostinfo.partition(":")
+        if not have_port:
+            port = None
+        return hostname, port
+
+
+class _NetlocResultMixinBytes(_NetlocResultMixinBase, _ResultMixinBytes):
+    __slots__ = ()
+
+    @property
+    def _userinfo(self):
+        netloc = self.netloc
+        userinfo, have_info, hostinfo = netloc.rpartition(b"@")
+        if have_info:
+            username, have_password, password = userinfo.partition(b":")
+            if not have_password:
+                password = None
+        else:
+            username = password = None
+        return username, password
+
+    @property
+    def _hostinfo(self):
+        netloc = self.netloc
+        _, _, hostinfo = netloc.rpartition(b"@")
+        _, have_open_br, bracketed = hostinfo.partition(b"[")
+        if have_open_br:
+            hostname, _, port = bracketed.partition(b"]")
+            _, have_port, port = port.partition(b":")
+        else:
+            hostname, have_port, port = hostinfo.partition(b":")
+        if not have_port:
+            port = None
+        return hostname, port
+
+
+from collections import namedtuple
+
+_DefragResultBase = namedtuple("DefragResult", "url fragment")
+_SplitResultBase = namedtuple("SplitResult", "scheme netloc path query fragment")
+_ParseResultBase = namedtuple("ParseResult", "scheme netloc path params query fragment")
+
+# For backwards compatibility, alias _NetlocResultMixinStr
+# ResultBase is no longer part of the documented API, but it is
+# retained since deprecating it isn't worth the hassle
+ResultBase = _NetlocResultMixinStr
+
+
+# Structured result objects for string data
+class DefragResult(_DefragResultBase, _ResultMixinStr):
+    __slots__ = ()
+
+    def geturl(self):
+        if self.fragment:
+            return self.url + "#" + self.fragment
+        else:
+            return self.url
+
+
+class SplitResult(_SplitResultBase, _NetlocResultMixinStr):
+    __slots__ = ()
+
+    def geturl(self):
+        return urlunsplit(self)
+
+
+class ParseResult(_ParseResultBase, _NetlocResultMixinStr):
+    __slots__ = ()
+
+    def geturl(self):
+        return urlunparse(self)
+
+
+# Structured result objects for bytes data
+class DefragResultBytes(_DefragResultBase, _ResultMixinBytes):
+    __slots__ = ()
+
+    def geturl(self):
+        if self.fragment:
+            return self.url + b"#" + self.fragment
+        else:
+            return self.url
+
+
+class SplitResultBytes(_SplitResultBase, _NetlocResultMixinBytes):
+    __slots__ = ()
+
+    def geturl(self):
+        return urlunsplit(self)
+
+
+class ParseResultBytes(_ParseResultBase, _NetlocResultMixinBytes):
+    __slots__ = ()
+
+    def geturl(self):
+        return urlunparse(self)
+
+
+# Set up the encode/decode result pairs
+def _fix_result_transcoding():
+    _result_pairs = (
+        (DefragResult, DefragResultBytes),
+        (SplitResult, SplitResultBytes),
+        (ParseResult, ParseResultBytes),
+    )
+    for _decoded, _encoded in _result_pairs:
+        _decoded._encoded_counterpart = _encoded
+        _encoded._decoded_counterpart = _decoded
+
+
+_fix_result_transcoding()
+del _fix_result_transcoding
+
+
+def urlparse(url, scheme="", allow_fragments=True):
+    """Parse a URL into 6 components:
+    <scheme>://<netloc>/<path>;<params>?<query>#<fragment>
+    Return a 6-tuple: (scheme, netloc, path, params, query, fragment).
+    Note that we don't break the components up in smaller bits
+    (e.g. netloc is a single string) and we don't expand % escapes."""
+    url, scheme, _coerce_result = _coerce_args(url, scheme)
+    splitresult = urlsplit(url, scheme, allow_fragments)
+    scheme, netloc, url, query, fragment = splitresult
+    if scheme in uses_params and ";" in url:
+        url, params = _splitparams(url)
+    else:
+        params = ""
+    result = ParseResult(scheme, netloc, url, params, query, fragment)
+    return _coerce_result(result)
+
+
+def _splitparams(url):
+    if "/" in url:
+        i = url.find(";", url.rfind("/"))
+        if i < 0:
+            return url, ""
+    else:
+        i = url.find(";")
+    return url[:i], url[i + 1 :]
+
+
+def _splitnetloc(url, start=0):
+    delim = len(url)  # position of end of domain part of url, default is end
+    for c in "/?#":  # look for delimiters; the order is NOT important
+        wdelim = url.find(c, start)  # find first of this delim
+        if wdelim >= 0:  # if found
+            delim = min(delim, wdelim)  # use earliest delim position
+    return url[start:delim], url[delim:]  # return (domain, rest)
+
+
+def urlsplit(url, scheme="", allow_fragments=True):
+    """Parse a URL into 5 components:
+    <scheme>://<netloc>/<path>?<query>#<fragment>
+    Return a 5-tuple: (scheme, netloc, path, query, fragment).
+    Note that we don't break the components up in smaller bits
+    (e.g. netloc is a single string) and we don't expand % escapes."""
+    url, scheme, _coerce_result = _coerce_args(url, scheme)
+    allow_fragments = bool(allow_fragments)
+    key = url, scheme, allow_fragments, type(url), type(scheme)
+    cached = _parse_cache.get(key, None)
+    if cached:
+        return _coerce_result(cached)
+    if len(_parse_cache) >= MAX_CACHE_SIZE:  # avoid runaway growth
+        clear_cache()
+    netloc = query = fragment = ""
+    i = url.find(":")
+    if i > 0:
+        if url[:i] == "http":  # optimize the common case
+            scheme = url[:i].lower()
+            url = url[i + 1 :]
+            if url[:2] == "//":
+                netloc, url = _splitnetloc(url, 2)
+                if ("[" in netloc and "]" not in netloc) or ("]" in netloc and "[" not in netloc):
+                    raise ValueError("Invalid IPv6 URL")
+            if allow_fragments and "#" in url:
+                url, fragment = url.split("#", 1)
+            if "?" in url:
+                url, query = url.split("?", 1)
+            v = SplitResult(scheme, netloc, url, query, fragment)
+            _parse_cache[key] = v
+            return _coerce_result(v)
+        for c in url[:i]:
+            if c not in scheme_chars:
+                break
+        else:
+            # make sure "url" is not actually a port number (in which case
+            # "scheme" is really part of the path)
+            rest = url[i + 1 :]
+            if not rest or any(c not in "0123456789" for c in rest):
+                # not a port number
+                scheme, url = url[:i].lower(), rest
+
+    if url[:2] == "//":
+        netloc, url = _splitnetloc(url, 2)
+        if ("[" in netloc and "]" not in netloc) or ("]" in netloc and "[" not in netloc):
+            raise ValueError("Invalid IPv6 URL")
+    if allow_fragments and "#" in url:
+        url, fragment = url.split("#", 1)
+    if "?" in url:
+        url, query = url.split("?", 1)
+    v = SplitResult(scheme, netloc, url, query, fragment)
+    _parse_cache[key] = v
+    return _coerce_result(v)
+
+
+def urlunparse(components):
+    """Put a parsed URL back together again.  This may result in a
+    slightly different, but equivalent URL, if the URL that was parsed
+    originally had redundant delimiters, e.g. a ? with an empty query
+    (the draft states that these are equivalent)."""
+    scheme, netloc, url, params, query, fragment, _coerce_result = _coerce_args(*components)
+    if params:
+        url = "%s;%s" % (url, params)
+    return _coerce_result(urlunsplit((scheme, netloc, url, query, fragment)))
+
+
+def urlunsplit(components):
+    """Combine the elements of a tuple as returned by urlsplit() into a
+    complete URL as a string. The data argument can be any five-item iterable.
+    This may result in a slightly different, but equivalent URL, if the URL that
+    was parsed originally had unnecessary delimiters (for example, a ? with an
+    empty query; the RFC states that these are equivalent)."""
+    scheme, netloc, url, query, fragment, _coerce_result = _coerce_args(*components)
+    if netloc or (scheme and scheme in uses_netloc and url[:2] != "//"):
+        if url and url[:1] != "/":
+            url = "/" + url
+        url = "//" + (netloc or "") + url
+    if scheme:
+        url = scheme + ":" + url
+    if query:
+        url = url + "?" + query
+    if fragment:
+        url = url + "#" + fragment
+    return _coerce_result(url)
+
+
+def urljoin(base, url, allow_fragments=True):
+    """Join a base URL and a possibly relative URL to form an absolute
+    interpretation of the latter."""
+    if not base:
+        return url
+    if not url:
+        return base
+    base, url, _coerce_result = _coerce_args(base, url)
+    bscheme, bnetloc, bpath, bparams, bquery, bfragment = urlparse(base, "", allow_fragments)
+    scheme, netloc, path, params, query, fragment = urlparse(url, bscheme, allow_fragments)
+    if scheme != bscheme or scheme not in uses_relative:
+        return _coerce_result(url)
+    if scheme in uses_netloc:
+        if netloc:
+            return _coerce_result(urlunparse((scheme, netloc, path, params, query, fragment)))
+        netloc = bnetloc
+    if path[:1] == "/":
+        return _coerce_result(urlunparse((scheme, netloc, path, params, query, fragment)))
+    if not path and not params:
+        path = bpath
+        params = bparams
+        if not query:
+            query = bquery
+        return _coerce_result(urlunparse((scheme, netloc, path, params, query, fragment)))
+    segments = bpath.split("/")[:-1] + path.split("/")
+    # XXX The stuff below is bogus in various ways...
+    if segments[-1] == ".":
+        segments[-1] = ""
+    while "." in segments:
+        segments.remove(".")
+    while 1:
+        i = 1
+        n = len(segments) - 1
+        while i < n:
+            if segments[i] == ".." and segments[i - 1] not in ("", ".."):
+                del segments[i - 1 : i + 1]
+                break
+            i = i + 1
+        else:
+            break
+    if segments == ["", ".."]:
+        segments[-1] = ""
+    elif len(segments) >= 2 and segments[-1] == "..":
+        segments[-2:] = [""]
+    return _coerce_result(
+        urlunparse((scheme, netloc, "/".join(segments), params, query, fragment))
+    )
+
+
+def urldefrag(url):
+    """Removes any existing fragment from URL.
+
+    Returns a tuple of the defragmented URL and the fragment.  If
+    the URL contained no fragments, the second element is the
+    empty string.
+    """
+    url, _coerce_result = _coerce_args(url)
+    if "#" in url:
+        s, n, p, a, q, frag = urlparse(url)
+        defrag = urlunparse((s, n, p, a, q, ""))
+    else:
+        frag = ""
+        defrag = url
+    return _coerce_result(DefragResult(defrag, frag))
+
+
+_hexdig = "0123456789ABCDEFabcdef"
+_hextobyte = {(a + b).encode(): bytes([int(a + b, 16)]) for a in _hexdig for b in _hexdig}
+
+
+def unquote_to_bytes(string):
+    """unquote_to_bytes('abc%20def') -> b'abc def'."""
+    # Note: strings are encoded as UTF-8. This is only an issue if it contains
+    # unescaped non-ASCII characters, which URIs should not.
+    if not string:
+        # Is it a string-like object?
+        string.split
+        return b""
+    if isinstance(string, str):
+        string = string.encode("utf-8")
+    bits = string.split(b"%")
+    if len(bits) == 1:
+        return string
+    res = [bits[0]]
+    append = res.append
+    for item in bits[1:]:
+        try:
+            append(_hextobyte[item[:2]])
+            append(item[2:])
+        except KeyError:
+            append(b"%")
+            append(item)
+    return b"".join(res)
+
+
+_asciire = re.compile(r"([\x00-\x7f]+)")
+
+
+def unquote(string, encoding="utf-8", errors="replace"):
+    """Replace %xx escapes by their single-character equivalent. The optional
+    encoding and errors parameters specify how to decode percent-encoded
+    sequences into Unicode characters, as accepted by the bytes.decode()
+    method.
+    By default, percent-encoded sequences are decoded with UTF-8, and invalid
+    sequences are replaced by a placeholder character.
+
+    unquote('abc%20def') -> 'abc def'.
+    """
+    if "%" not in string:
+        string.split
+        return string
+    if encoding is None:
+        encoding = "utf-8"
+    if errors is None:
+        errors = "replace"
+    bits = _asciire.split(string)
+    res = [bits[0]]
+    append = res.append
+    for i in range(1, len(bits), 2):
+        append(unquote_to_bytes(bits[i]).decode(encoding, errors))
+        append(bits[i + 1])
+    return "".join(res)
+
+
+def parse_qs(
+    qs, keep_blank_values=False, strict_parsing=False, encoding="utf-8", errors="replace"
+):
+    """Parse a query given as a string argument.
+
+    Arguments:
+
+    qs: percent-encoded query string to be parsed
+
+    keep_blank_values: flag indicating whether blank values in
+        percent-encoded queries should be treated as blank strings.
+        A true value indicates that blanks should be retained as
+        blank strings.  The default false value indicates that
+        blank values are to be ignored and treated as if they were
+        not included.
+
+    strict_parsing: flag indicating what to do with parsing errors.
+        If false (the default), errors are silently ignored.
+        If true, errors raise a ValueError exception.
+
+    encoding and errors: specify how to decode percent-encoded sequences
+        into Unicode characters, as accepted by the bytes.decode() method.
+    """
+    parsed_result = {}
+    pairs = parse_qsl(qs, keep_blank_values, strict_parsing, encoding=encoding, errors=errors)
+    for name, value in pairs:
+        if name in parsed_result:
+            parsed_result[name].append(value)
+        else:
+            parsed_result[name] = [value]
+    return parsed_result
+
+
+def parse_qsl(
+    qs, keep_blank_values=False, strict_parsing=False, encoding="utf-8", errors="replace"
+):
+    """Parse a query given as a string argument.
+
+    Arguments:
+
+    qs: percent-encoded query string to be parsed
+
+    keep_blank_values: flag indicating whether blank values in
+        percent-encoded queries should be treated as blank strings.  A
+        true value indicates that blanks should be retained as blank
+        strings.  The default false value indicates that blank values
+        are to be ignored and treated as if they were  not included.
+
+    strict_parsing: flag indicating what to do with parsing errors. If
+        false (the default), errors are silently ignored. If true,
+        errors raise a ValueError exception.
+
+    encoding and errors: specify how to decode percent-encoded sequences
+        into Unicode characters, as accepted by the bytes.decode() method.
+
+    Returns a list, as G-d intended.
+    """
+    qs, _coerce_result = _coerce_args(qs)
+    pairs = [s2 for s1 in qs.split("&") for s2 in s1.split(";")]
+    r = []
+    for name_value in pairs:
+        if not name_value and not strict_parsing:
+            continue
+        nv = name_value.split("=", 1)
+        if len(nv) != 2:
+            if strict_parsing:
+                raise ValueError("bad query field: %r" % (name_value,))
+            # Handle case of a control-name with no equal sign
+            if keep_blank_values:
+                nv.append("")
+            else:
+                continue
+        if len(nv[1]) or keep_blank_values:
+            name = nv[0].replace("+", " ")
+            name = unquote(name, encoding=encoding, errors=errors)
+            name = _coerce_result(name)
+            value = nv[1].replace("+", " ")
+            value = unquote(value, encoding=encoding, errors=errors)
+            value = _coerce_result(value)
+            r.append((name, value))
+    return r
+
+
+def unquote_plus(string, encoding="utf-8", errors="replace"):
+    """Like unquote(), but also replace plus signs by spaces, as required for
+    unquoting HTML form values.
+
+    unquote_plus('%7e/abc+def') -> '~/abc def'
+    """
+    string = string.replace("+", " ")
+    return unquote(string, encoding, errors)
+
+
+_ALWAYS_SAFE = frozenset(
+    b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" b"abcdefghijklmnopqrstuvwxyz" b"0123456789" b"_.-"
+)
+_ALWAYS_SAFE_BYTES = bytes(_ALWAYS_SAFE)
+_safe_quoters = {}
+
+
+class Quoter(collections.defaultdict):
+    """A mapping from bytes (in range(0,256)) to strings.
+
+    String values are percent-encoded byte values, unless the key < 128, and
+    in the "safe" set (either the specified safe set, or default set).
+    """
+
+    # Keeps a cache internally, using defaultdict, for efficiency (lookups
+    # of cached keys don't call Python code at all).
+    def __init__(self, safe):
+        """safe: bytes object."""
+        self.safe = _ALWAYS_SAFE.union(safe)
+
+    def __repr__(self):
+        # Without this, will just display as a defaultdict
+        return "<Quoter %r>" % dict(self)
+
+    def __missing__(self, b):
+        # Handle a cache miss. Store quoted string in cache and return.
+        res = chr(b) if b in self.safe else "%{:02X}".format(b)
+        self[b] = res
+        return res
+
+
+def quote(string, safe="/", encoding=None, errors=None):
+    """quote('abc def') -> 'abc%20def'
+
+    Each part of a URL, e.g. the path info, the query, etc., has a
+    different set of reserved characters that must be quoted.
+
+    RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
+    the following reserved characters.
+
+    reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
+                  "$" | ","
+
+    Each of these characters is reserved in some component of a URL,
+    but not necessarily in all of them.
+
+    By default, the quote function is intended for quoting the path
+    section of a URL.  Thus, it will not encode '/'.  This character
+    is reserved, but in typical usage the quote function is being
+    called on a path where the existing slash characters are used as
+    reserved characters.
+
+    string and safe may be either str or bytes objects. encoding must
+    not be specified if string is a str.
+
+    The optional encoding and errors parameters specify how to deal with
+    non-ASCII characters, as accepted by the str.encode method.
+    By default, encoding='utf-8' (characters are encoded with UTF-8), and
+    errors='strict' (unsupported characters raise a UnicodeEncodeError).
+    """
+    if isinstance(string, str):
+        if not string:
+            return string
+        if encoding is None:
+            encoding = "utf-8"
+        if errors is None:
+            errors = "strict"
+        string = string.encode(encoding, errors)
+    else:
+        if encoding is not None:
+            raise TypeError("quote() doesn't support 'encoding' for bytes")
+        if errors is not None:
+            raise TypeError("quote() doesn't support 'errors' for bytes")
+    return quote_from_bytes(string, safe)
+
+
+def quote_plus(string, safe="", encoding=None, errors=None):
+    """Like quote(), but also replace ' ' with '+', as required for quoting
+    HTML form values. Plus signs in the original string are escaped unless
+    they are included in safe. It also does not have safe default to '/'.
+    """
+    # Check if ' ' in string, where string may either be a str or bytes.  If
+    # there are no spaces, the regular quote will produce the right answer.
+    if (isinstance(string, str) and " " not in string) or (
+        isinstance(string, bytes) and b" " not in string
+    ):
+        return quote(string, safe, encoding, errors)
+    if isinstance(safe, str):
+        space = " "
+    else:
+        space = b" "
+    string = quote(string, safe + space, encoding, errors)
+    return string.replace(" ", "+")
+
+
+def quote_from_bytes(bs, safe="/"):
+    """Like quote(), but accepts a bytes object rather than a str, and does
+    not perform string-to-bytes encoding.  It always returns an ASCII string.
+    quote_from_bytes(b'abc def\x3f') -> 'abc%20def%3f'
+    """
+    if not isinstance(bs, (bytes, bytearray)):
+        raise TypeError("quote_from_bytes() expected bytes")
+    if not bs:
+        return ""
+    if isinstance(safe, str):
+        # Normalize 'safe' by converting to bytes and removing non-ASCII chars
+        safe = safe.encode("ascii", "ignore")
+    else:
+        safe = bytes([c for c in safe if c < 128])
+    if not bs.rstrip(_ALWAYS_SAFE_BYTES + safe):
+        return bs.decode()
+    try:
+        quoter = _safe_quoters[safe]
+    except KeyError:
+        _safe_quoters[safe] = quoter = Quoter(safe).__getitem__
+    return "".join([quoter(char) for char in bs])
+
+
+def urlencode(query, doseq=False, safe="", encoding=None, errors=None):
+    """Encode a dict or sequence of two-element tuples into a URL query string.
+
+    If any values in the query arg are sequences and doseq is true, each
+    sequence element is converted to a separate parameter.
+
+    If the query arg is a sequence of two-element tuples, the order of the
+    parameters in the output will match the order of parameters in the
+    input.
+
+    The components of a query arg may each be either a string or a bytes type.
+    When a component is a string, the safe, encoding and error parameters are
+    sent to the quote_plus function for encoding.
+    """
+
+    if hasattr(query, "items"):
+        query = query.items()
+    else:
+        # It's a bother at times that strings and string-like objects are
+        # sequences.
+        try:
+            # non-sequence items should not work with len()
+            # non-empty strings will fail this
+            if len(query) and not isinstance(query[0], tuple):
+                raise TypeError
+            # Zero-length sequences of all types will get here and succeed,
+            # but that's a minor nit.  Since the original implementation
+            # allowed empty dicts that type of behavior probably should be
+            # preserved for consistency
+        except TypeError:
+            #            ty, va, tb = sys.exc_info()
+            raise TypeError(
+                "not a valid non-string sequence " "or mapping object"
+            )  # .with_traceback(tb)
+
+    l = []
+    if not doseq:
+        for k, v in query:
+            if isinstance(k, bytes):
+                k = quote_plus(k, safe)
+            else:
+                k = quote_plus(str(k), safe, encoding, errors)
+
+            if isinstance(v, bytes):
+                v = quote_plus(v, safe)
+            else:
+                v = quote_plus(str(v), safe, encoding, errors)
+            l.append(k + "=" + v)
+    else:
+        for k, v in query:
+            if isinstance(k, bytes):
+                k = quote_plus(k, safe)
+            else:
+                k = quote_plus(str(k), safe, encoding, errors)
+
+            if isinstance(v, bytes):
+                v = quote_plus(v, safe)
+                l.append(k + "=" + v)
+            elif isinstance(v, str):
+                v = quote_plus(v, safe, encoding, errors)
+                l.append(k + "=" + v)
+            else:
+                try:
+                    # Is this a sufficient test for sequence-ness?
+                    x = len(v)
+                except TypeError:
+                    # not a sequence
+                    v = quote_plus(str(v), safe, encoding, errors)
+                    l.append(k + "=" + v)
+                else:
+                    # loop over the sequence
+                    for elt in v:
+                        if isinstance(elt, bytes):
+                            elt = quote_plus(elt, safe)
+                        else:
+                            elt = quote_plus(str(elt), safe, encoding, errors)
+                        l.append(k + "=" + elt)
+    return "&".join(l)
+
+
+# Utilities to parse URLs (most of these return None for missing parts):
+# unwrap('<URL:type://host/path>') --> 'type://host/path'
+# splittype('type:opaquestring') --> 'type', 'opaquestring'
+# splithost('//host[:port]/path') --> 'host[:port]', '/path'
+# splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'
+# splitpasswd('user:passwd') -> 'user', 'passwd'
+# splitport('host:port') --> 'host', 'port'
+# splitquery('/path?query') --> '/path', 'query'
+# splittag('/path#tag') --> '/path', 'tag'
+# splitattr('/path;attr1=value1;attr2=value2;...') ->
+#   '/path', ['attr1=value1', 'attr2=value2', ...]
+# splitvalue('attr=value') --> 'attr', 'value'
+# urllib.parse.unquote('abc%20def') -> 'abc def'
+# quote('abc def') -> 'abc%20def')
+
+
+def to_bytes(url):
+    """to_bytes(u"URL") --> 'URL'."""
+    # Most URL schemes require ASCII. If that changes, the conversion
+    # can be relaxed.
+    # XXX get rid of to_bytes()
+    if isinstance(url, str):
+        try:
+            url = url.encode("ASCII").decode()
+        except UnicodeError:
+            raise UnicodeError("URL " + repr(url) + " contains non-ASCII characters")
+    return url
+
+
+def unwrap(url):
+    """unwrap('<URL:type://host/path>') --> 'type://host/path'."""
+    url = str(url).strip()
+    if url[:1] == "<" and url[-1:] == ">":
+        url = url[1:-1].strip()
+    if url[:4] == "URL:":
+        url = url[4:].strip()
+    return url
+
+
+_typeprog = None
+
+
+def splittype(url):
+    """splittype('type:opaquestring') --> 'type', 'opaquestring'."""
+    global _typeprog
+    if _typeprog is None:
+        import re
+
+        _typeprog = re.compile("^([^/:]+):")
+
+    match = _typeprog.match(url)
+    if match:
+        scheme = match.group(1)
+        return scheme.lower(), url[len(scheme) + 1 :]
+    return None, url
+
+
+_hostprog = None
+
+
+def splithost(url):
+    """splithost('//host[:port]/path') --> 'host[:port]', '/path'."""
+    global _hostprog
+    if _hostprog is None:
+        import re
+
+        _hostprog = re.compile("^//([^/?]*)(.*)$")
+
+    match = _hostprog.match(url)
+    if match:
+        host_port = match.group(1)
+        path = match.group(2)
+        if path and not path.startswith("/"):
+            path = "/" + path
+        return host_port, path
+    return None, url
+
+
+_userprog = None
+
+
+def splituser(host):
+    """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'."""
+    global _userprog
+    if _userprog is None:
+        import re
+
+        _userprog = re.compile("^(.*)@(.*)$")
+
+    match = _userprog.match(host)
+    if match:
+        return match.group(1, 2)
+    return None, host
+
+
+_passwdprog = None
+
+
+def splitpasswd(user):
+    """splitpasswd('user:passwd') -> 'user', 'passwd'."""
+    global _passwdprog
+    if _passwdprog is None:
+        import re
+
+        _passwdprog = re.compile("^([^:]*):(.*)$", re.S)
+
+    match = _passwdprog.match(user)
+    if match:
+        return match.group(1, 2)
+    return user, None
+
+
+# splittag('/path#tag') --> '/path', 'tag'
+_portprog = None
+
+
+def splitport(host):
+    """splitport('host:port') --> 'host', 'port'."""
+    global _portprog
+    if _portprog is None:
+        import re
+
+        _portprog = re.compile("^(.*):([0-9]+)$")
+
+    match = _portprog.match(host)
+    if match:
+        return match.group(1, 2)
+    return host, None
+
+
+_nportprog = None
+
+
+def splitnport(host, defport=-1):
+    """Split host and port, returning numeric port.
+    Return given default port if no ':' found; defaults to -1.
+    Return numerical port if a valid number are found after ':'.
+    Return None if ':' but not a valid number."""
+    global _nportprog
+    if _nportprog is None:
+        import re
+
+        _nportprog = re.compile("^(.*):(.*)$")
+
+    match = _nportprog.match(host)
+    if match:
+        host, port = match.group(1, 2)
+        try:
+            if not port:
+                raise ValueError("no digits")
+            nport = int(port)
+        except ValueError:
+            nport = None
+        return host, nport
+    return host, defport
+
+
+_queryprog = None
+
+
+def splitquery(url):
+    """splitquery('/path?query') --> '/path', 'query'."""
+    global _queryprog
+    if _queryprog is None:
+        import re
+
+        _queryprog = re.compile("^(.*)\?([^?]*)$")
+
+    match = _queryprog.match(url)
+    if match:
+        return match.group(1, 2)
+    return url, None
+
+
+_tagprog = None
+
+
+def splittag(url):
+    """splittag('/path#tag') --> '/path', 'tag'."""
+    global _tagprog
+    if _tagprog is None:
+        import re
+
+        _tagprog = re.compile("^(.*)#([^#]*)$")
+
+    match = _tagprog.match(url)
+    if match:
+        return match.group(1, 2)
+    return url, None
+
+
+def splitattr(url):
+    """splitattr('/path;attr1=value1;attr2=value2;...') ->
+    '/path', ['attr1=value1', 'attr2=value2', ...]."""
+    words = url.split(";")
+    return words[0], words[1:]
+
+
+_valueprog = None
+
+
+def splitvalue(attr):
+    """splitvalue('attr=value') --> 'attr', 'value'."""
+    global _valueprog
+    if _valueprog is None:
+        import re
+
+        _valueprog = re.compile("^([^=]*)=(.*)$")
+
+    match = _valueprog.match(attr)
+    if match:
+        return match.group(1, 2)
+    return attr, None
diff --git a/upip/Makefile b/upip/Makefile
deleted file mode 100644
index 15d0f9ede..000000000
--- a/upip/Makefile
+++ /dev/null
@@ -1,23 +0,0 @@
-all:
-
-# This target prepares snapshot of all dependency modules, for
-# self-contained install
-deps: upip_errno.py upip_gzip.py upip_utarfile.py
-
-upip_ffilib.py: ../ffilib/ffilib.py
-	cp $^ $@
-upip_os.py: ../os/os/__init__.py
-	sed -r -e 's/((ffilib|errno|stat)([^_"]|$$))/upip_\1/' $^ >$@
-upip_os_path.py: ../os.path/os/path.py
-	sed -r -e 's/((ffilib|errno|os|stat)([^_"]|$$))/upip_\1/' $^ >$@
-upip_errno.py: ../errno/errno.py
-	cp $^ $@
-upip_gzip.py: ../gzip/gzip.py
-	cp $^ $@
-upip_stat.py: ../stat/stat.py
-	cp $^ $@
-upip_utarfile.py: ../utarfile/utarfile.py
-	cp $^ $@
-
-clean:
-	rm upip_*.py
diff --git a/upip/bootstrap_upip.sh b/upip/bootstrap_upip.sh
deleted file mode 100755
index 35446b9f2..000000000
--- a/upip/bootstrap_upip.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-# This script performs bootstrap installation of upip package manager from PyPI
-# All the other packages can be installed using it.
-
-if [ -z "$TMPDIR" ]; then
-    cd /tmp
-else
-    cd $TMPDIR
-fi
-
-# Remove any stale old version
-rm -rf micropython-upip-*
-wget -nd -r -l1 https://pypi.python.org/pypi/micropython-upip/ --accept-regex ".*pypi.python.org/packages/source/.*.gz" --reject=html
-
-tar xfz micropython-upip-*.tar.gz
-mkdir -p ~/.micropython/lib/
-cp micropython-upip-*/upip*.py ~/.micropython/lib/
-
-echo "upip is installed. To use:"
-echo "micropython -m upip --help"
diff --git a/upip/metadata.txt b/upip/metadata.txt
deleted file mode 100644
index f26dfa9c0..000000000
--- a/upip/metadata.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.6.3
-author = Paul Sokolovsky
-extra_modules = upip_errno, upip_gzip, upip_utarfile
-desc = Simple package manager for MicroPython.
-long_desc = Simple package manager for MicroPython, targetting to be self-hosted (but not yet there). Compatible only with packages without custom setup.py code.
diff --git a/upip/setup.py b/upip/setup.py
deleted file mode 100644
index 5cc511ade..000000000
--- a/upip/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-upip',
-      version='0.6.3',
-      description='Simple package manager for MicroPython.',
-      long_description='Simple package manager for MicroPython, targetting to be self-hosted (but not yet there). Compatible only with packages without custom setup.py code.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['upip', 'upip_errno', 'upip_gzip', 'upip_utarfile'])
diff --git a/upip/upip.py b/upip/upip.py
deleted file mode 100644
index 666b2a1d4..000000000
--- a/upip/upip.py
+++ /dev/null
@@ -1,291 +0,0 @@
-def upip_import(mod, sub=None):
-    try:
-        mod_ = mod
-        if sub:
-            mod_ += "_" + sub
-        return __import__("upip_" + mod_)
-    except ImportError:
-        m = __import__(mod)
-        if sub:
-            return getattr(m, sub)
-        return m
-
-sys = upip_import("sys")
-import uos as os
-
-errno = upip_import("errno")
-gzip = upip_import("gzip")
-try:
-    tarfile = upip_import("utarfile")
-except ImportError:
-    tarfile = upip_import("tarfile")
-try:
-    json = upip_import("ujson")
-except ImportError:
-    json = upip_import("json")
-
-
-DEFAULT_MICROPYPATH = "~/.micropython/lib:/usr/lib/micropython"
-
-debug = False
-cleanup_files = [".pkg.tar"]
-
-class NotFoundError(Exception):
-    pass
-
-def op_split(path):
-    if path == "":
-        return ("", "")
-    r = path.rsplit("/", 1)
-    if len(r) == 1:
-        return ("", path)
-    head = r[0]
-    if not head:
-        head = "/"
-    return (head, r[1])
-
-def op_basename(path):
-    return op_split(path)[1]
-
-def _makedirs(name, mode=0o777):
-    ret = False
-    s = ""
-    for c in name.rstrip("/").split("/"):
-        s += c + "/"
-        try:
-            os.mkdir(s)
-            ret = True
-        except OSError as e:
-            if e.args[0] != errno.EEXIST and e.args[0] != errno.EISDIR:
-                raise
-            ret = False
-    return ret
-
-
-def save_file(fname, subf):
-    outf = open(fname, "wb")
-    while True:
-        buf = subf.read(1024)
-        if not buf:
-            break
-        outf.write(buf)
-    outf.close()
-
-def install_tar(f, prefix):
-    meta = {}
-    for info in f:
-        #print(info)
-        fname = info.name
-        try:
-            fname = fname[fname.index("/") + 1:]
-        except ValueError:
-            fname = ""
-
-        save = True
-        for p in ("setup.", "PKG-INFO", "README"):
-                #print(fname, p)
-                if fname.startswith(p) or ".egg-info" in fname:
-                    if fname.endswith("/requires.txt"):
-                        meta["deps"] = f.extractfile(info).read()
-                    save = False
-                    if debug:
-                        print("Skipping", fname)
-                    break
-
-        if save:
-            outfname = prefix + fname
-            if info.type == tarfile.DIRTYPE:
-                if _makedirs(outfname):
-                    print("Created " + outfname)
-            else:
-                if debug:
-                    print("Extracting " + outfname)
-                subf = f.extractfile(info)
-                save_file(outfname, subf)
-    return meta
-
-def expandhome(s):
-    h = os.getenv("HOME")
-    s = s.replace("~/", h + "/")
-    return s
-
-try:
-    import ussl
-    import usocket
-    def download(url, local_name):
-        proto, _, host, urlpath = url.split('/', 3)
-        ai = usocket.getaddrinfo(host, 443)
-        #print("Address infos:", ai)
-        addr = ai[0][4]
-
-        s = usocket.socket()
-        #print("Connect address:", addr)
-        s.connect(addr)
-
-        if proto == "https:":
-            s = ussl.wrap_socket(s)
-
-        # MicroPython rawsocket module supports file interface directly
-        s.write("GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n" % (urlpath, host))
-        l = s.readline()
-        protover, status, msg = l.split(None, 2)
-        if status != b"200":
-            raise OSError()
-        while 1:
-            l = s.readline()
-            if not l:
-                raise OSError()
-            if l == b'\r\n':
-                break
-        with open(local_name, "wb") as f:
-            while 1:
-                l = s.read(1024)
-                if not l:
-                    break
-                f.write(l)
-
-except ImportError:
-
-    def download(url, local_name):
-        if debug:
-            print("wget -q %s -O %s" % (url, local_name))
-        rc = os.system("wget -q %s -O %s" % (url, local_name))
-        if local_name not in cleanup_files:
-            cleanup_files.append(local_name)
-        if rc == 8 * 256:
-            raise NotFoundError
-
-
-def get_pkg_metadata(name):
-    download("https://pypi.python.org/pypi/%s/json" % name, ".pkg.json")
-    with open(".pkg.json") as f:
-        s = f.read()
-    return json.loads(s)
-
-
-def fatal(msg):
-    print(msg)
-    sys.exit(1)
-
-def gzdecompress(package_fname):
-    f = open(package_fname, "rb")
-    zipdata = f.read()
-    data = gzip.decompress(zipdata)
-    return data
-
-def gzdecompress_(package_fname):
-    os.system("gzip -d -c %s > ungz" % package_fname)
-    with open("ungz", "rb") as f:
-        return f.read()
-
-def install_pkg(pkg_spec, install_path):
-    data = get_pkg_metadata(pkg_spec)
-
-    latest_ver = data["info"]["version"]
-    packages = data["releases"][latest_ver]
-    assert len(packages) == 1
-    package_url = packages[0]["url"]
-    print("Installing %s %s from %s" % (pkg_spec, latest_ver, package_url))
-    package_fname = op_basename(package_url)
-    download(package_url, package_fname)
-
-    data = gzdecompress(package_fname)
-
-    f = open(".pkg.tar", "wb")
-    f.write(data)
-    f.close()
-
-    f = tarfile.TarFile(".pkg.tar")
-    return install_tar(f, install_path)
-
-def cleanup():
-    for fname in cleanup_files:
-        try:
-            os.unlink(fname)
-        except OSError:
-            print("Warning: Cannot delete " + fname)
-
-def help():
-    print("upip - Simple PyPI package manager for MicroPython")
-    print("Usage: micropython -m upip install <package>... | -r <requirements.txt>")
-    print("""\
-Note: only MicroPython packages (usually, micropython-*) are supported for
-installation, upip does not support arbitrary code in setup.py.""")
-    sys.exit(1)
-
-def main():
-    global debug
-    install_path = None
-
-    if len(sys.argv) < 2 or sys.argv[1] == "-h" or sys.argv[1] == "--help":
-        help()
-
-    if sys.argv[1] != "install":
-        fatal("Only 'install' command supported")
-
-    to_install = []
-
-    i = 2
-    while i < len(sys.argv) and sys.argv[i][0] == "-":
-        opt = sys.argv[i]
-        i += 1
-        if opt == "-h" or opt == "--help":
-            help()
-        elif opt == "-p":
-            install_path = sys.argv[i]
-            i += 1
-        elif opt == "-r":
-            list_file = sys.argv[i]
-            i += 1
-            with open(list_file) as f:
-                while True:
-                    l = f.readline()
-                    if not l:
-                        break
-                    to_install.append(l.rstrip())
-        elif opt == "--debug":
-            debug = True
-        else:
-            fatal("Unknown/unsupported option: " + opt)
-
-    if install_path is None:
-        install_path = os.getenv("MICROPYPATH") or DEFAULT_MICROPYPATH
-
-    install_path = install_path.split(":", 1)[0]
-
-    install_path = expandhome(install_path)
-
-    if install_path[-1] != "/":
-        install_path += "/"
-
-    print("Installing to: " + install_path)
-
-    to_install.extend(sys.argv[i:])
-    if not to_install:
-        help()
-
-    # sets would be perfect here, but don't depend on them
-    installed = []
-    try:
-        while to_install:
-            if debug:
-                print("Queue:", to_install)
-            pkg_spec = to_install.pop(0)
-            if pkg_spec in installed:
-                continue
-            meta = install_pkg(pkg_spec, install_path)
-            installed.append(pkg_spec)
-            if debug:
-                print(meta)
-            deps = meta.get("deps", "").rstrip()
-            if deps:
-                deps = deps.decode("utf-8").split("\n")
-                to_install.extend(deps)
-    except NotFoundError:
-        print("Error: cannot find '%s' package (or server error), packages may be partially installed" \
-            % pkg_spec, file=sys.stderr)
-
-    if not debug:
-        cleanup()
-
-main()
diff --git a/upip/upip_errno.py b/upip/upip_errno.py
deleted file mode 100644
index 7b7935ef8..000000000
--- a/upip/upip_errno.py
+++ /dev/null
@@ -1,38 +0,0 @@
-EPERM   = 1   # Operation not permitted
-ENOENT  = 2   # No such file or directory
-ESRCH   = 3   # No such process
-EINTR   = 4   # Interrupted system call
-EIO     = 5   # I/O error
-ENXIO   = 6   # No such device or address
-E2BIG   = 7   # Argument list too long
-ENOEXEC = 8   # Exec format error
-EBADF   = 9   # Bad file number
-ECHILD  = 10  # No child processes
-EAGAIN  = 11  # Try again
-ENOMEM  = 12  # Out of memory
-EACCES  = 13  # Permission denied
-EFAULT  = 14  # Bad address
-ENOTBLK = 15  # Block device required
-EBUSY   = 16  # Device or resource busy
-EEXIST  = 17  # File exists
-EXDEV   = 18  # Cross-device link
-ENODEV  = 19  # No such device
-ENOTDIR = 20  # Not a directory
-EISDIR  = 21  # Is a directory
-EINVAL  = 22  # Invalid argument
-ENFILE  = 23  # File table overflow
-EMFILE  = 24  # Too many open files
-ENOTTY  = 25  # Not a typewriter
-ETXTBSY = 26  # Text file busy
-EFBIG   = 27  # File too large
-ENOSPC  = 28  # No space left on device
-ESPIPE  = 29  # Illegal seek
-EROFS   = 30  # Read-only file system
-EMLINK  = 31  # Too many links
-EPIPE   = 32  # Broken pipe
-EDOM    = 33  # Math argument out of domain of func
-ERANGE  = 34  # Math result not representable
-EAFNOSUPPORT = 97 # Address family not supported by protocol
-ECONNRESET = 104 # Connection timed out
-ETIMEDOUT = 110 # Connection timed out
-EINPROGRESS = 115 # Operation now in progress
diff --git a/upip/upip_gzip.py b/upip/upip_gzip.py
deleted file mode 100644
index be4e8f4a5..000000000
--- a/upip/upip_gzip.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#import zlib
-import uzlib as zlib
-
-FTEXT    = 1
-FHCRC    = 2
-FEXTRA   = 4
-FNAME    = 8
-FCOMMENT = 16
-
-def decompress(data):
-    assert data[0] == 0x1f and data[1] == 0x8b
-    assert data[2] == 8
-    flg = data[3]
-    assert flg & 0xe0 == 0
-    i = 10
-    if flg & FEXTRA:
-        i += data[11] << 8 + data[10] + 2
-    if flg & FNAME:
-        while data[i]:
-            i += 1
-        i += 1
-    if flg & FCOMMENT:
-        while data[i]:
-            i += 1
-        i += 1
-    if flg & FHCRC:
-        i += 2
-    return zlib.decompress(memoryview(data)[i:], -15)
diff --git a/upip/upip_utarfile.py b/upip/upip_utarfile.py
deleted file mode 100644
index 2c3ab7f50..000000000
--- a/upip/upip_utarfile.py
+++ /dev/null
@@ -1,82 +0,0 @@
-import uctypes
-
-# http://www.gnu.org/software/tar/manual/html_node/Standard.html
-TAR_HEADER = {
-    "name": (uctypes.ARRAY | 0, uctypes.UINT8 | 100),
-    "size": (uctypes.ARRAY | 124, uctypes.UINT8 | 12),
-}
-
-DIRTYPE = "dir"
-REGTYPE = "file"
-
-def roundup(val, align):
-    return (val + align - 1) & ~(align - 1)
-
-def skip(f, size):
-    assert size % 512 == 0
-    buf = bytearray(512)
-    while size:
-        size -= f.readinto(buf)
-
-class FileSection:
-
-    def __init__(self, f, content_len, aligned_len):
-        self.f = f
-        self.content_len = content_len
-        self.align = aligned_len - content_len
-
-    def read(self, sz=65536):
-        if self.content_len == 0:
-            return b""
-        if sz > self.content_len:
-            sz = self.content_len
-        data = self.f.read(sz)
-        sz = len(data)
-        self.content_len -= sz
-        return data
-
-    def skip(self):
-        self.f.read(self.content_len + self.align)
-
-class TarInfo:
-
-    def __str__(self):
-        return "TarInfo(%r, %s, %d)" % (self.name, self.type, self.size)
-
-class TarFile:
-
-    def __init__(self, name):
-        self.f = open(name, "rb")
-        self.subf = None
-
-    def next(self):
-            if self.subf:
-                self.subf.skip()
-            buf = self.f.read(512)
-            if not buf:
-                return None
-
-            h = uctypes.struct(uctypes.addressof(buf), TAR_HEADER, uctypes.LITTLE_ENDIAN)
-
-            # Empty block means end of archive
-            if h.name[0] == 0:
-                return None
-
-            d = TarInfo()
-            d.name = str(h.name, "utf-8").rstrip()
-            d.size = int(bytes(h.size).rstrip(), 8)
-            d.type = [REGTYPE, DIRTYPE][d.name[-1] == "/"]
-            self.subf = d.subf = FileSection(self.f, d.size, roundup(d.size, 512))
-            return d
-
-    def __iter__(self):
-        return self
-
-    def __next__(self):
-        v = self.next()
-        if v is None:
-            raise StopIteration
-        return v
-
-    def extractfile(self, tarinfo):
-        return tarinfo.subf
diff --git a/urllib.parse/metadata.txt b/urllib.parse/metadata.txt
deleted file mode 100644
index 48c011ccc..000000000
--- a/urllib.parse/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = package
-version = 0.5.1
-depends = re-pcre, collections, collections.defaultdict
diff --git a/urllib.parse/setup.py b/urllib.parse/setup.py
deleted file mode 100644
index 3f88a3553..000000000
--- a/urllib.parse/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-urllib.parse',
-      version='0.5.1',
-      description='CPython urllib.parse module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      packages=['urllib'],
-      install_requires=['micropython-re-pcre', 'micropython-collections', 'micropython-collections.defaultdict'])
diff --git a/urllib.parse/test_urlparse.py b/urllib.parse/test_urlparse.py
deleted file mode 100644
index 7689bc119..000000000
--- a/urllib.parse/test_urlparse.py
+++ /dev/null
@@ -1,855 +0,0 @@
-#! /usr/bin/env python3
-
-from test import support
-import unittest
-import urllib.parse
-
-RFC1808_BASE = "http://a/b/c/d;p?q#f"
-RFC2396_BASE = "http://a/b/c/d;p?q"
-RFC3986_BASE = 'http://a/b/c/d;p?q'
-SIMPLE_BASE  = 'http://a/b/c/d'
-
-# A list of test cases.  Each test case is a two-tuple that contains
-# a string with the query and a dictionary with the expected result.
-
-parse_qsl_test_cases = [
-    ("", []),
-    ("&", []),
-    ("&&", []),
-    ("=", [('', '')]),
-    ("=a", [('', 'a')]),
-    ("a", [('a', '')]),
-    ("a=", [('a', '')]),
-    ("a=", [('a', '')]),
-    ("&a=b", [('a', 'b')]),
-    ("a=a+b&b=b+c", [('a', 'a b'), ('b', 'b c')]),
-    ("a=1&a=2", [('a', '1'), ('a', '2')]),
-    (b"", []),
-    (b"&", []),
-    (b"&&", []),
-    (b"=", [(b'', b'')]),
-    (b"=a", [(b'', b'a')]),
-    (b"a", [(b'a', b'')]),
-    (b"a=", [(b'a', b'')]),
-    (b"a=", [(b'a', b'')]),
-    (b"&a=b", [(b'a', b'b')]),
-    (b"a=a+b&b=b+c", [(b'a', b'a b'), (b'b', b'b c')]),
-    (b"a=1&a=2", [(b'a', b'1'), (b'a', b'2')]),
-]
-
-class UrlParseTestCase(unittest.TestCase):
-
-    def checkRoundtrips(self, url, parsed, split):
-        result = urllib.parse.urlparse(url)
-        self.assertEqual(result, parsed)
-        t = (result.scheme, result.netloc, result.path,
-             result.params, result.query, result.fragment)
-        self.assertEqual(t, parsed)
-        # put it back together and it should be the same
-        result2 = urllib.parse.urlunparse(result)
-        self.assertEqual(result2, url)
-        self.assertEqual(result2, result.geturl())
-
-        # the result of geturl() is a fixpoint; we can always parse it
-        # again to get the same result:
-        result3 = urllib.parse.urlparse(result.geturl())
-        self.assertEqual(result3.geturl(), result.geturl())
-        self.assertEqual(result3,          result)
-        self.assertEqual(result3.scheme,   result.scheme)
-        self.assertEqual(result3.netloc,   result.netloc)
-        self.assertEqual(result3.path,     result.path)
-        self.assertEqual(result3.params,   result.params)
-        self.assertEqual(result3.query,    result.query)
-        self.assertEqual(result3.fragment, result.fragment)
-        self.assertEqual(result3.username, result.username)
-        self.assertEqual(result3.password, result.password)
-        self.assertEqual(result3.hostname, result.hostname)
-        self.assertEqual(result3.port,     result.port)
-
-        # check the roundtrip using urlsplit() as well
-        result = urllib.parse.urlsplit(url)
-        self.assertEqual(result, split)
-        t = (result.scheme, result.netloc, result.path,
-             result.query, result.fragment)
-        self.assertEqual(t, split)
-        result2 = urllib.parse.urlunsplit(result)
-        self.assertEqual(result2, url)
-        self.assertEqual(result2, result.geturl())
-
-        # check the fixpoint property of re-parsing the result of geturl()
-        result3 = urllib.parse.urlsplit(result.geturl())
-        self.assertEqual(result3.geturl(), result.geturl())
-        self.assertEqual(result3,          result)
-        self.assertEqual(result3.scheme,   result.scheme)
-        self.assertEqual(result3.netloc,   result.netloc)
-        self.assertEqual(result3.path,     result.path)
-        self.assertEqual(result3.query,    result.query)
-        self.assertEqual(result3.fragment, result.fragment)
-        self.assertEqual(result3.username, result.username)
-        self.assertEqual(result3.password, result.password)
-        self.assertEqual(result3.hostname, result.hostname)
-        self.assertEqual(result3.port,     result.port)
-
-    def test_qsl(self):
-        for orig, expect in parse_qsl_test_cases:
-            result = urllib.parse.parse_qsl(orig, keep_blank_values=True)
-            self.assertEqual(result, expect, "Error parsing %r" % orig)
-            expect_without_blanks = [v for v in expect if len(v[1])]
-            result = urllib.parse.parse_qsl(orig, keep_blank_values=False)
-            self.assertEqual(result, expect_without_blanks,
-                            "Error parsing %r" % orig)
-
-    def test_roundtrips(self):
-        str_cases = [
-            ('file:///tmp/junk.txt',
-             ('file', '', '/tmp/junk.txt', '', '', ''),
-             ('file', '', '/tmp/junk.txt', '', '')),
-            ('imap://mail.python.org/mbox1',
-             ('imap', 'mail.python.org', '/mbox1', '', '', ''),
-             ('imap', 'mail.python.org', '/mbox1', '', '')),
-            ('mms://wms.sys.hinet.net/cts/Drama/09006251100.asf',
-             ('mms', 'wms.sys.hinet.net', '/cts/Drama/09006251100.asf',
-              '', '', ''),
-             ('mms', 'wms.sys.hinet.net', '/cts/Drama/09006251100.asf',
-              '', '')),
-            ('nfs://server/path/to/file.txt',
-             ('nfs', 'server', '/path/to/file.txt', '', '', ''),
-             ('nfs', 'server', '/path/to/file.txt', '', '')),
-            ('svn+ssh://svn.zope.org/repos/main/ZConfig/trunk/',
-             ('svn+ssh', 'svn.zope.org', '/repos/main/ZConfig/trunk/',
-              '', '', ''),
-             ('svn+ssh', 'svn.zope.org', '/repos/main/ZConfig/trunk/',
-              '', '')),
-            ('git+ssh://git@github.com/user/project.git',
-            ('git+ssh', 'git@github.com','/user/project.git',
-             '','',''),
-            ('git+ssh', 'git@github.com','/user/project.git',
-             '', '')),
-            ]
-        def _encode(t):
-            return (t[0].encode('ascii'),
-                    tuple(x.encode('ascii') for x in t[1]),
-                    tuple(x.encode('ascii') for x in t[2]))
-        bytes_cases = [_encode(x) for x in str_cases]
-        for url, parsed, split in str_cases + bytes_cases:
-            self.checkRoundtrips(url, parsed, split)
-
-    def test_http_roundtrips(self):
-        # urllib.parse.urlsplit treats 'http:' as an optimized special case,
-        # so we test both 'http:' and 'https:' in all the following.
-        # Three cheers for white box knowledge!
-        str_cases = [
-            ('://www.python.org',
-             ('www.python.org', '', '', '', ''),
-             ('www.python.org', '', '', '')),
-            ('://www.python.org#abc',
-             ('www.python.org', '', '', '', 'abc'),
-             ('www.python.org', '', '', 'abc')),
-            ('://www.python.org?q=abc',
-             ('www.python.org', '', '', 'q=abc', ''),
-             ('www.python.org', '', 'q=abc', '')),
-            ('://www.python.org/#abc',
-             ('www.python.org', '/', '', '', 'abc'),
-             ('www.python.org', '/', '', 'abc')),
-            ('://a/b/c/d;p?q#f',
-             ('a', '/b/c/d', 'p', 'q', 'f'),
-             ('a', '/b/c/d;p', 'q', 'f')),
-            ]
-        def _encode(t):
-            return (t[0].encode('ascii'),
-                    tuple(x.encode('ascii') for x in t[1]),
-                    tuple(x.encode('ascii') for x in t[2]))
-        bytes_cases = [_encode(x) for x in str_cases]
-        str_schemes = ('http', 'https')
-        bytes_schemes = (b'http', b'https')
-        str_tests = str_schemes, str_cases
-        bytes_tests = bytes_schemes, bytes_cases
-        for schemes, test_cases in (str_tests, bytes_tests):
-            for scheme in schemes:
-                for url, parsed, split in test_cases:
-                    url = scheme + url
-                    parsed = (scheme,) + parsed
-                    split = (scheme,) + split
-                    self.checkRoundtrips(url, parsed, split)
-
-    def checkJoin(self, base, relurl, expected):
-        str_components = (base, relurl, expected)
-        self.assertEqual(urllib.parse.urljoin(base, relurl), expected)
-        bytes_components = baseb, relurlb, expectedb = [
-                            x.encode('ascii') for x in str_components]
-        self.assertEqual(urllib.parse.urljoin(baseb, relurlb), expectedb)
-
-    def test_unparse_parse(self):
-        str_cases = ['Python', './Python','x-newscheme://foo.com/stuff','x://y','x:/y','x:/','/',]
-        bytes_cases = [x.encode('ascii') for x in str_cases]
-        for u in str_cases + bytes_cases:
-            self.assertEqual(urllib.parse.urlunsplit(urllib.parse.urlsplit(u)), u)
-            self.assertEqual(urllib.parse.urlunparse(urllib.parse.urlparse(u)), u)
-
-    def test_RFC1808(self):
-        # "normal" cases from RFC 1808:
-        self.checkJoin(RFC1808_BASE, 'g:h', 'g:h')
-        self.checkJoin(RFC1808_BASE, 'g', 'http://a/b/c/g')
-        self.checkJoin(RFC1808_BASE, './g', 'http://a/b/c/g')
-        self.checkJoin(RFC1808_BASE, 'g/', 'http://a/b/c/g/')
-        self.checkJoin(RFC1808_BASE, '/g', 'http://a/g')
-        self.checkJoin(RFC1808_BASE, '//g', 'http://g')
-        self.checkJoin(RFC1808_BASE, 'g?y', 'http://a/b/c/g?y')
-        self.checkJoin(RFC1808_BASE, 'g?y/./x', 'http://a/b/c/g?y/./x')
-        self.checkJoin(RFC1808_BASE, '#s', 'http://a/b/c/d;p?q#s')
-        self.checkJoin(RFC1808_BASE, 'g#s', 'http://a/b/c/g#s')
-        self.checkJoin(RFC1808_BASE, 'g#s/./x', 'http://a/b/c/g#s/./x')
-        self.checkJoin(RFC1808_BASE, 'g?y#s', 'http://a/b/c/g?y#s')
-        self.checkJoin(RFC1808_BASE, 'g;x', 'http://a/b/c/g;x')
-        self.checkJoin(RFC1808_BASE, 'g;x?y#s', 'http://a/b/c/g;x?y#s')
-        self.checkJoin(RFC1808_BASE, '.', 'http://a/b/c/')
-        self.checkJoin(RFC1808_BASE, './', 'http://a/b/c/')
-        self.checkJoin(RFC1808_BASE, '..', 'http://a/b/')
-        self.checkJoin(RFC1808_BASE, '../', 'http://a/b/')
-        self.checkJoin(RFC1808_BASE, '../g', 'http://a/b/g')
-        self.checkJoin(RFC1808_BASE, '../..', 'http://a/')
-        self.checkJoin(RFC1808_BASE, '../../', 'http://a/')
-        self.checkJoin(RFC1808_BASE, '../../g', 'http://a/g')
-
-        # "abnormal" cases from RFC 1808:
-        self.checkJoin(RFC1808_BASE, '', 'http://a/b/c/d;p?q#f')
-        self.checkJoin(RFC1808_BASE, '../../../g', 'http://a/../g')
-        self.checkJoin(RFC1808_BASE, '../../../../g', 'http://a/../../g')
-        self.checkJoin(RFC1808_BASE, '/./g', 'http://a/./g')
-        self.checkJoin(RFC1808_BASE, '/../g', 'http://a/../g')
-        self.checkJoin(RFC1808_BASE, 'g.', 'http://a/b/c/g.')
-        self.checkJoin(RFC1808_BASE, '.g', 'http://a/b/c/.g')
-        self.checkJoin(RFC1808_BASE, 'g..', 'http://a/b/c/g..')
-        self.checkJoin(RFC1808_BASE, '..g', 'http://a/b/c/..g')
-        self.checkJoin(RFC1808_BASE, './../g', 'http://a/b/g')
-        self.checkJoin(RFC1808_BASE, './g/.', 'http://a/b/c/g/')
-        self.checkJoin(RFC1808_BASE, 'g/./h', 'http://a/b/c/g/h')
-        self.checkJoin(RFC1808_BASE, 'g/../h', 'http://a/b/c/h')
-
-        # RFC 1808 and RFC 1630 disagree on these (according to RFC 1808),
-        # so we'll not actually run these tests (which expect 1808 behavior).
-        #self.checkJoin(RFC1808_BASE, 'http:g', 'http:g')
-        #self.checkJoin(RFC1808_BASE, 'http:', 'http:')
-
-    def test_RFC2368(self):
-        # Issue 11467: path that starts with a number is not parsed correctly
-        self.assertEqual(urllib.parse.urlparse('mailto:1337@example.org'),
-                ('mailto', '', '1337@example.org', '', '', ''))
-
-    def test_RFC2396(self):
-        # cases from RFC 2396
-
-
-        self.checkJoin(RFC2396_BASE, 'g:h', 'g:h')
-        self.checkJoin(RFC2396_BASE, 'g', 'http://a/b/c/g')
-        self.checkJoin(RFC2396_BASE, './g', 'http://a/b/c/g')
-        self.checkJoin(RFC2396_BASE, 'g/', 'http://a/b/c/g/')
-        self.checkJoin(RFC2396_BASE, '/g', 'http://a/g')
-        self.checkJoin(RFC2396_BASE, '//g', 'http://g')
-        self.checkJoin(RFC2396_BASE, 'g?y', 'http://a/b/c/g?y')
-        self.checkJoin(RFC2396_BASE, '#s', 'http://a/b/c/d;p?q#s')
-        self.checkJoin(RFC2396_BASE, 'g#s', 'http://a/b/c/g#s')
-        self.checkJoin(RFC2396_BASE, 'g?y#s', 'http://a/b/c/g?y#s')
-        self.checkJoin(RFC2396_BASE, 'g;x', 'http://a/b/c/g;x')
-        self.checkJoin(RFC2396_BASE, 'g;x?y#s', 'http://a/b/c/g;x?y#s')
-        self.checkJoin(RFC2396_BASE, '.', 'http://a/b/c/')
-        self.checkJoin(RFC2396_BASE, './', 'http://a/b/c/')
-        self.checkJoin(RFC2396_BASE, '..', 'http://a/b/')
-        self.checkJoin(RFC2396_BASE, '../', 'http://a/b/')
-        self.checkJoin(RFC2396_BASE, '../g', 'http://a/b/g')
-        self.checkJoin(RFC2396_BASE, '../..', 'http://a/')
-        self.checkJoin(RFC2396_BASE, '../../', 'http://a/')
-        self.checkJoin(RFC2396_BASE, '../../g', 'http://a/g')
-        self.checkJoin(RFC2396_BASE, '', RFC2396_BASE)
-        self.checkJoin(RFC2396_BASE, '../../../g', 'http://a/../g')
-        self.checkJoin(RFC2396_BASE, '../../../../g', 'http://a/../../g')
-        self.checkJoin(RFC2396_BASE, '/./g', 'http://a/./g')
-        self.checkJoin(RFC2396_BASE, '/../g', 'http://a/../g')
-        self.checkJoin(RFC2396_BASE, 'g.', 'http://a/b/c/g.')
-        self.checkJoin(RFC2396_BASE, '.g', 'http://a/b/c/.g')
-        self.checkJoin(RFC2396_BASE, 'g..', 'http://a/b/c/g..')
-        self.checkJoin(RFC2396_BASE, '..g', 'http://a/b/c/..g')
-        self.checkJoin(RFC2396_BASE, './../g', 'http://a/b/g')
-        self.checkJoin(RFC2396_BASE, './g/.', 'http://a/b/c/g/')
-        self.checkJoin(RFC2396_BASE, 'g/./h', 'http://a/b/c/g/h')
-        self.checkJoin(RFC2396_BASE, 'g/../h', 'http://a/b/c/h')
-        self.checkJoin(RFC2396_BASE, 'g;x=1/./y', 'http://a/b/c/g;x=1/y')
-        self.checkJoin(RFC2396_BASE, 'g;x=1/../y', 'http://a/b/c/y')
-        self.checkJoin(RFC2396_BASE, 'g?y/./x', 'http://a/b/c/g?y/./x')
-        self.checkJoin(RFC2396_BASE, 'g?y/../x', 'http://a/b/c/g?y/../x')
-        self.checkJoin(RFC2396_BASE, 'g#s/./x', 'http://a/b/c/g#s/./x')
-        self.checkJoin(RFC2396_BASE, 'g#s/../x', 'http://a/b/c/g#s/../x')
-
-    def test_RFC3986(self):
-        # Test cases from RFC3986
-        self.checkJoin(RFC3986_BASE, '?y','http://a/b/c/d;p?y')
-        self.checkJoin(RFC2396_BASE, ';x', 'http://a/b/c/;x')
-        self.checkJoin(RFC3986_BASE, 'g:h','g:h')
-        self.checkJoin(RFC3986_BASE, 'g','http://a/b/c/g')
-        self.checkJoin(RFC3986_BASE, './g','http://a/b/c/g')
-        self.checkJoin(RFC3986_BASE, 'g/','http://a/b/c/g/')
-        self.checkJoin(RFC3986_BASE, '/g','http://a/g')
-        self.checkJoin(RFC3986_BASE, '//g','http://g')
-        self.checkJoin(RFC3986_BASE, '?y','http://a/b/c/d;p?y')
-        self.checkJoin(RFC3986_BASE, 'g?y','http://a/b/c/g?y')
-        self.checkJoin(RFC3986_BASE, '#s','http://a/b/c/d;p?q#s')
-        self.checkJoin(RFC3986_BASE, 'g#s','http://a/b/c/g#s')
-        self.checkJoin(RFC3986_BASE, 'g?y#s','http://a/b/c/g?y#s')
-        self.checkJoin(RFC3986_BASE, ';x','http://a/b/c/;x')
-        self.checkJoin(RFC3986_BASE, 'g;x','http://a/b/c/g;x')
-        self.checkJoin(RFC3986_BASE, 'g;x?y#s','http://a/b/c/g;x?y#s')
-        self.checkJoin(RFC3986_BASE, '','http://a/b/c/d;p?q')
-        self.checkJoin(RFC3986_BASE, '.','http://a/b/c/')
-        self.checkJoin(RFC3986_BASE, './','http://a/b/c/')
-        self.checkJoin(RFC3986_BASE, '..','http://a/b/')
-        self.checkJoin(RFC3986_BASE, '../','http://a/b/')
-        self.checkJoin(RFC3986_BASE, '../g','http://a/b/g')
-        self.checkJoin(RFC3986_BASE, '../..','http://a/')
-        self.checkJoin(RFC3986_BASE, '../../','http://a/')
-        self.checkJoin(RFC3986_BASE, '../../g','http://a/g')
-
-        #Abnormal Examples
-
-        # The 'abnormal scenarios' are incompatible with RFC2986 parsing
-        # Tests are here for reference.
-
-        #self.checkJoin(RFC3986_BASE, '../../../g','http://a/g')
-        #self.checkJoin(RFC3986_BASE, '../../../../g','http://a/g')
-        #self.checkJoin(RFC3986_BASE, '/./g','http://a/g')
-        #self.checkJoin(RFC3986_BASE, '/../g','http://a/g')
-
-        self.checkJoin(RFC3986_BASE, 'g.','http://a/b/c/g.')
-        self.checkJoin(RFC3986_BASE, '.g','http://a/b/c/.g')
-        self.checkJoin(RFC3986_BASE, 'g..','http://a/b/c/g..')
-        self.checkJoin(RFC3986_BASE, '..g','http://a/b/c/..g')
-        self.checkJoin(RFC3986_BASE, './../g','http://a/b/g')
-        self.checkJoin(RFC3986_BASE, './g/.','http://a/b/c/g/')
-        self.checkJoin(RFC3986_BASE, 'g/./h','http://a/b/c/g/h')
-        self.checkJoin(RFC3986_BASE, 'g/../h','http://a/b/c/h')
-        self.checkJoin(RFC3986_BASE, 'g;x=1/./y','http://a/b/c/g;x=1/y')
-        self.checkJoin(RFC3986_BASE, 'g;x=1/../y','http://a/b/c/y')
-        self.checkJoin(RFC3986_BASE, 'g?y/./x','http://a/b/c/g?y/./x')
-        self.checkJoin(RFC3986_BASE, 'g?y/../x','http://a/b/c/g?y/../x')
-        self.checkJoin(RFC3986_BASE, 'g#s/./x','http://a/b/c/g#s/./x')
-        self.checkJoin(RFC3986_BASE, 'g#s/../x','http://a/b/c/g#s/../x')
-        #self.checkJoin(RFC3986_BASE, 'http:g','http:g') # strict parser
-        self.checkJoin(RFC3986_BASE, 'http:g','http://a/b/c/g') #relaxed parser
-
-        # Test for issue9721
-        self.checkJoin('http://a/b/c/de', ';x','http://a/b/c/;x')
-
-    def test_urljoins(self):
-        self.checkJoin(SIMPLE_BASE, 'g:h','g:h')
-        self.checkJoin(SIMPLE_BASE, 'http:g','http://a/b/c/g')
-        self.checkJoin(SIMPLE_BASE, 'http:','http://a/b/c/d')
-        self.checkJoin(SIMPLE_BASE, 'g','http://a/b/c/g')
-        self.checkJoin(SIMPLE_BASE, './g','http://a/b/c/g')
-        self.checkJoin(SIMPLE_BASE, 'g/','http://a/b/c/g/')
-        self.checkJoin(SIMPLE_BASE, '/g','http://a/g')
-        self.checkJoin(SIMPLE_BASE, '//g','http://g')
-        self.checkJoin(SIMPLE_BASE, '?y','http://a/b/c/d?y')
-        self.checkJoin(SIMPLE_BASE, 'g?y','http://a/b/c/g?y')
-        self.checkJoin(SIMPLE_BASE, 'g?y/./x','http://a/b/c/g?y/./x')
-        self.checkJoin(SIMPLE_BASE, '.','http://a/b/c/')
-        self.checkJoin(SIMPLE_BASE, './','http://a/b/c/')
-        self.checkJoin(SIMPLE_BASE, '..','http://a/b/')
-        self.checkJoin(SIMPLE_BASE, '../','http://a/b/')
-        self.checkJoin(SIMPLE_BASE, '../g','http://a/b/g')
-        self.checkJoin(SIMPLE_BASE, '../..','http://a/')
-        self.checkJoin(SIMPLE_BASE, '../../g','http://a/g')
-        self.checkJoin(SIMPLE_BASE, '../../../g','http://a/../g')
-        self.checkJoin(SIMPLE_BASE, './../g','http://a/b/g')
-        self.checkJoin(SIMPLE_BASE, './g/.','http://a/b/c/g/')
-        self.checkJoin(SIMPLE_BASE, '/./g','http://a/./g')
-        self.checkJoin(SIMPLE_BASE, 'g/./h','http://a/b/c/g/h')
-        self.checkJoin(SIMPLE_BASE, 'g/../h','http://a/b/c/h')
-        self.checkJoin(SIMPLE_BASE, 'http:g','http://a/b/c/g')
-        self.checkJoin(SIMPLE_BASE, 'http:','http://a/b/c/d')
-        self.checkJoin(SIMPLE_BASE, 'http:?y','http://a/b/c/d?y')
-        self.checkJoin(SIMPLE_BASE, 'http:g?y','http://a/b/c/g?y')
-        self.checkJoin(SIMPLE_BASE, 'http:g?y/./x','http://a/b/c/g?y/./x')
-        self.checkJoin('http:///', '..','http:///')
-        self.checkJoin('', 'http://a/b/c/g?y/./x','http://a/b/c/g?y/./x')
-        self.checkJoin('', 'http://a/./g', 'http://a/./g')
-        self.checkJoin('svn://pathtorepo/dir1', 'dir2', 'svn://pathtorepo/dir2')
-        self.checkJoin('svn+ssh://pathtorepo/dir1', 'dir2', 'svn+ssh://pathtorepo/dir2')
-
-    def test_RFC2732(self):
-        str_cases = [
-            ('http://Test.python.org:5432/foo/', 'test.python.org', 5432),
-            ('http://12.34.56.78:5432/foo/', '12.34.56.78', 5432),
-            ('http://[::1]:5432/foo/', '::1', 5432),
-            ('http://[dead:beef::1]:5432/foo/', 'dead:beef::1', 5432),
-            ('http://[dead:beef::]:5432/foo/', 'dead:beef::', 5432),
-            ('http://[dead:beef:cafe:5417:affe:8FA3:deaf:feed]:5432/foo/',
-             'dead:beef:cafe:5417:affe:8fa3:deaf:feed', 5432),
-            ('http://[::12.34.56.78]:5432/foo/', '::12.34.56.78', 5432),
-            ('http://[::ffff:12.34.56.78]:5432/foo/',
-             '::ffff:12.34.56.78', 5432),
-            ('http://Test.python.org/foo/', 'test.python.org', None),
-            ('http://12.34.56.78/foo/', '12.34.56.78', None),
-            ('http://[::1]/foo/', '::1', None),
-            ('http://[dead:beef::1]/foo/', 'dead:beef::1', None),
-            ('http://[dead:beef::]/foo/', 'dead:beef::', None),
-            ('http://[dead:beef:cafe:5417:affe:8FA3:deaf:feed]/foo/',
-             'dead:beef:cafe:5417:affe:8fa3:deaf:feed', None),
-            ('http://[::12.34.56.78]/foo/', '::12.34.56.78', None),
-            ('http://[::ffff:12.34.56.78]/foo/',
-             '::ffff:12.34.56.78', None),
-            ]
-        def _encode(t):
-            return t[0].encode('ascii'), t[1].encode('ascii'), t[2]
-        bytes_cases = [_encode(x) for x in str_cases]
-        for url, hostname, port in str_cases + bytes_cases:
-            urlparsed = urllib.parse.urlparse(url)
-            self.assertEqual((urlparsed.hostname, urlparsed.port) , (hostname, port))
-
-        str_cases = [
-                'http://::12.34.56.78]/',
-                'http://[::1/foo/',
-                'ftp://[::1/foo/bad]/bad',
-                'http://[::1/foo/bad]/bad',
-                'http://[::ffff:12.34.56.78']
-        bytes_cases = [x.encode('ascii') for x in str_cases]
-        for invalid_url in str_cases + bytes_cases:
-            self.assertRaises(ValueError, urllib.parse.urlparse, invalid_url)
-
-    def test_urldefrag(self):
-        str_cases = [
-            ('http://python.org#frag', 'http://python.org', 'frag'),
-            ('http://python.org', 'http://python.org', ''),
-            ('http://python.org/#frag', 'http://python.org/', 'frag'),
-            ('http://python.org/', 'http://python.org/', ''),
-            ('http://python.org/?q#frag', 'http://python.org/?q', 'frag'),
-            ('http://python.org/?q', 'http://python.org/?q', ''),
-            ('http://python.org/p#frag', 'http://python.org/p', 'frag'),
-            ('http://python.org/p?q', 'http://python.org/p?q', ''),
-            (RFC1808_BASE, 'http://a/b/c/d;p?q', 'f'),
-            (RFC2396_BASE, 'http://a/b/c/d;p?q', ''),
-        ]
-        def _encode(t):
-            return type(t)(x.encode('ascii') for x in t)
-        bytes_cases = [_encode(x) for x in str_cases]
-        for url, defrag, frag in str_cases + bytes_cases:
-            result = urllib.parse.urldefrag(url)
-            self.assertEqual(result.geturl(), url)
-            self.assertEqual(result, (defrag, frag))
-            self.assertEqual(result.url, defrag)
-            self.assertEqual(result.fragment, frag)
-
-    def test_urlsplit_attributes(self):
-        url = "HTTP://WWW.PYTHON.ORG/doc/#frag"
-        p = urllib.parse.urlsplit(url)
-        self.assertEqual(p.scheme, "http")
-        self.assertEqual(p.netloc, "WWW.PYTHON.ORG")
-        self.assertEqual(p.path, "/doc/")
-        self.assertEqual(p.query, "")
-        self.assertEqual(p.fragment, "frag")
-        self.assertEqual(p.username, None)
-        self.assertEqual(p.password, None)
-        self.assertEqual(p.hostname, "www.python.org")
-        self.assertEqual(p.port, None)
-        # geturl() won't return exactly the original URL in this case
-        # since the scheme is always case-normalized
-        # We handle this by ignoring the first 4 characters of the URL
-        self.assertEqual(p.geturl()[4:], url[4:])
-
-        url = "http://User:Pass@www.python.org:080/doc/?query=yes#frag"
-        p = urllib.parse.urlsplit(url)
-        self.assertEqual(p.scheme, "http")
-        self.assertEqual(p.netloc, "User:Pass@www.python.org:080")
-        self.assertEqual(p.path, "/doc/")
-        self.assertEqual(p.query, "query=yes")
-        self.assertEqual(p.fragment, "frag")
-        self.assertEqual(p.username, "User")
-        self.assertEqual(p.password, "Pass")
-        self.assertEqual(p.hostname, "www.python.org")
-        self.assertEqual(p.port, 80)
-        self.assertEqual(p.geturl(), url)
-
-        # Addressing issue1698, which suggests Username can contain
-        # "@" characters.  Though not RFC compliant, many ftp sites allow
-        # and request email addresses as usernames.
-
-        url = "http://User@example.com:Pass@www.python.org:080/doc/?query=yes#frag"
-        p = urllib.parse.urlsplit(url)
-        self.assertEqual(p.scheme, "http")
-        self.assertEqual(p.netloc, "User@example.com:Pass@www.python.org:080")
-        self.assertEqual(p.path, "/doc/")
-        self.assertEqual(p.query, "query=yes")
-        self.assertEqual(p.fragment, "frag")
-        self.assertEqual(p.username, "User@example.com")
-        self.assertEqual(p.password, "Pass")
-        self.assertEqual(p.hostname, "www.python.org")
-        self.assertEqual(p.port, 80)
-        self.assertEqual(p.geturl(), url)
-
-        # And check them all again, only with bytes this time
-        url = b"HTTP://WWW.PYTHON.ORG/doc/#frag"
-        p = urllib.parse.urlsplit(url)
-        self.assertEqual(p.scheme, b"http")
-        self.assertEqual(p.netloc, b"WWW.PYTHON.ORG")
-        self.assertEqual(p.path, b"/doc/")
-        self.assertEqual(p.query, b"")
-        self.assertEqual(p.fragment, b"frag")
-        self.assertEqual(p.username, None)
-        self.assertEqual(p.password, None)
-        self.assertEqual(p.hostname, b"www.python.org")
-        self.assertEqual(p.port, None)
-        self.assertEqual(p.geturl()[4:], url[4:])
-
-        url = b"http://User:Pass@www.python.org:080/doc/?query=yes#frag"
-        p = urllib.parse.urlsplit(url)
-        self.assertEqual(p.scheme, b"http")
-        self.assertEqual(p.netloc, b"User:Pass@www.python.org:080")
-        self.assertEqual(p.path, b"/doc/")
-        self.assertEqual(p.query, b"query=yes")
-        self.assertEqual(p.fragment, b"frag")
-        self.assertEqual(p.username, b"User")
-        self.assertEqual(p.password, b"Pass")
-        self.assertEqual(p.hostname, b"www.python.org")
-        self.assertEqual(p.port, 80)
-        self.assertEqual(p.geturl(), url)
-
-        url = b"http://User@example.com:Pass@www.python.org:080/doc/?query=yes#frag"
-        p = urllib.parse.urlsplit(url)
-        self.assertEqual(p.scheme, b"http")
-        self.assertEqual(p.netloc, b"User@example.com:Pass@www.python.org:080")
-        self.assertEqual(p.path, b"/doc/")
-        self.assertEqual(p.query, b"query=yes")
-        self.assertEqual(p.fragment, b"frag")
-        self.assertEqual(p.username, b"User@example.com")
-        self.assertEqual(p.password, b"Pass")
-        self.assertEqual(p.hostname, b"www.python.org")
-        self.assertEqual(p.port, 80)
-        self.assertEqual(p.geturl(), url)
-
-        # Verify an illegal port is returned as None
-        url = b"HTTP://WWW.PYTHON.ORG:65536/doc/#frag"
-        p = urllib.parse.urlsplit(url)
-        self.assertEqual(p.port, None)
-
-    def test_attributes_bad_port(self):
-        """Check handling of non-integer ports."""
-        p = urllib.parse.urlsplit("http://www.example.net:foo")
-        self.assertEqual(p.netloc, "www.example.net:foo")
-        self.assertRaises(ValueError, lambda: p.port)
-
-        p = urllib.parse.urlparse("http://www.example.net:foo")
-        self.assertEqual(p.netloc, "www.example.net:foo")
-        self.assertRaises(ValueError, lambda: p.port)
-
-        # Once again, repeat ourselves to test bytes
-        p = urllib.parse.urlsplit(b"http://www.example.net:foo")
-        self.assertEqual(p.netloc, b"www.example.net:foo")
-        self.assertRaises(ValueError, lambda: p.port)
-
-        p = urllib.parse.urlparse(b"http://www.example.net:foo")
-        self.assertEqual(p.netloc, b"www.example.net:foo")
-        self.assertRaises(ValueError, lambda: p.port)
-
-    def test_attributes_without_netloc(self):
-        # This example is straight from RFC 3261.  It looks like it
-        # should allow the username, hostname, and port to be filled
-        # in, but doesn't.  Since it's a URI and doesn't use the
-        # scheme://netloc syntax, the netloc and related attributes
-        # should be left empty.
-        uri = "sip:alice@atlanta.com;maddr=239.255.255.1;ttl=15"
-        p = urllib.parse.urlsplit(uri)
-        self.assertEqual(p.netloc, "")
-        self.assertEqual(p.username, None)
-        self.assertEqual(p.password, None)
-        self.assertEqual(p.hostname, None)
-        self.assertEqual(p.port, None)
-        self.assertEqual(p.geturl(), uri)
-
-        p = urllib.parse.urlparse(uri)
-        self.assertEqual(p.netloc, "")
-        self.assertEqual(p.username, None)
-        self.assertEqual(p.password, None)
-        self.assertEqual(p.hostname, None)
-        self.assertEqual(p.port, None)
-        self.assertEqual(p.geturl(), uri)
-
-        # You guessed it, repeating the test with bytes input
-        uri = b"sip:alice@atlanta.com;maddr=239.255.255.1;ttl=15"
-        p = urllib.parse.urlsplit(uri)
-        self.assertEqual(p.netloc, b"")
-        self.assertEqual(p.username, None)
-        self.assertEqual(p.password, None)
-        self.assertEqual(p.hostname, None)
-        self.assertEqual(p.port, None)
-        self.assertEqual(p.geturl(), uri)
-
-        p = urllib.parse.urlparse(uri)
-        self.assertEqual(p.netloc, b"")
-        self.assertEqual(p.username, None)
-        self.assertEqual(p.password, None)
-        self.assertEqual(p.hostname, None)
-        self.assertEqual(p.port, None)
-        self.assertEqual(p.geturl(), uri)
-
-    def test_noslash(self):
-        # Issue 1637: http://foo.com?query is legal
-        self.assertEqual(urllib.parse.urlparse("http://example.com?blahblah=/foo"),
-                         ('http', 'example.com', '', '', 'blahblah=/foo', ''))
-        self.assertEqual(urllib.parse.urlparse(b"http://example.com?blahblah=/foo"),
-                         (b'http', b'example.com', b'', b'', b'blahblah=/foo', b''))
-
-    def test_withoutscheme(self):
-        # Test urlparse without scheme
-        # Issue 754016: urlparse goes wrong with IP:port without scheme
-        # RFC 1808 specifies that netloc should start with //, urlparse expects
-        # the same, otherwise it classifies the portion of url as path.
-        self.assertEqual(urllib.parse.urlparse("path"),
-                ('','','path','','',''))
-        self.assertEqual(urllib.parse.urlparse("//www.python.org:80"),
-                ('','www.python.org:80','','','',''))
-        self.assertEqual(urllib.parse.urlparse("http://www.python.org:80"),
-                ('http','www.python.org:80','','','',''))
-        # Repeat for bytes input
-        self.assertEqual(urllib.parse.urlparse(b"path"),
-                (b'',b'',b'path',b'',b'',b''))
-        self.assertEqual(urllib.parse.urlparse(b"//www.python.org:80"),
-                (b'',b'www.python.org:80',b'',b'',b'',b''))
-        self.assertEqual(urllib.parse.urlparse(b"http://www.python.org:80"),
-                (b'http',b'www.python.org:80',b'',b'',b'',b''))
-
-    def test_portseparator(self):
-        # Issue 754016 makes changes for port separator ':' from scheme separator
-        self.assertEqual(urllib.parse.urlparse("path:80"),
-                ('','','path:80','','',''))
-        self.assertEqual(urllib.parse.urlparse("http:"),('http','','','','',''))
-        self.assertEqual(urllib.parse.urlparse("https:"),('https','','','','',''))
-        self.assertEqual(urllib.parse.urlparse("http://www.python.org:80"),
-                ('http','www.python.org:80','','','',''))
-        # As usual, need to check bytes input as well
-        self.assertEqual(urllib.parse.urlparse(b"path:80"),
-                (b'',b'',b'path:80',b'',b'',b''))
-        self.assertEqual(urllib.parse.urlparse(b"http:"),(b'http',b'',b'',b'',b'',b''))
-        self.assertEqual(urllib.parse.urlparse(b"https:"),(b'https',b'',b'',b'',b'',b''))
-        self.assertEqual(urllib.parse.urlparse(b"http://www.python.org:80"),
-                (b'http',b'www.python.org:80',b'',b'',b'',b''))
-
-    def test_usingsys(self):
-        # Issue 3314: sys module is used in the error
-        self.assertRaises(TypeError, urllib.parse.urlencode, "foo")
-
-    def test_anyscheme(self):
-        # Issue 7904: s3://foo.com/stuff has netloc "foo.com".
-        self.assertEqual(urllib.parse.urlparse("s3://foo.com/stuff"),
-                         ('s3', 'foo.com', '/stuff', '', '', ''))
-        self.assertEqual(urllib.parse.urlparse("x-newscheme://foo.com/stuff"),
-                         ('x-newscheme', 'foo.com', '/stuff', '', '', ''))
-        self.assertEqual(urllib.parse.urlparse("x-newscheme://foo.com/stuff?query#fragment"),
-                         ('x-newscheme', 'foo.com', '/stuff', '', 'query', 'fragment'))
-        self.assertEqual(urllib.parse.urlparse("x-newscheme://foo.com/stuff?query"),
-                         ('x-newscheme', 'foo.com', '/stuff', '', 'query', ''))
-
-        # And for bytes...
-        self.assertEqual(urllib.parse.urlparse(b"s3://foo.com/stuff"),
-                         (b's3', b'foo.com', b'/stuff', b'', b'', b''))
-        self.assertEqual(urllib.parse.urlparse(b"x-newscheme://foo.com/stuff"),
-                         (b'x-newscheme', b'foo.com', b'/stuff', b'', b'', b''))
-        self.assertEqual(urllib.parse.urlparse(b"x-newscheme://foo.com/stuff?query#fragment"),
-                         (b'x-newscheme', b'foo.com', b'/stuff', b'', b'query', b'fragment'))
-        self.assertEqual(urllib.parse.urlparse(b"x-newscheme://foo.com/stuff?query"),
-                         (b'x-newscheme', b'foo.com', b'/stuff', b'', b'query', b''))
-
-    def _test_mixed_types_rejected(self):
-        # Several functions that process either strings or ASCII encoded bytes
-        # accept multiple arguments. Check they reject mixed type input
-        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
-            urllib.parse.urlparse("www.python.org", b"http")
-        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
-            urllib.parse.urlparse(b"www.python.org", "http")
-        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
-            urllib.parse.urlsplit("www.python.org", b"http")
-        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
-            urllib.parse.urlsplit(b"www.python.org", "http")
-        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
-            urllib.parse.urlunparse(( b"http", "www.python.org","","","",""))
-        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
-            urllib.parse.urlunparse(("http", b"www.python.org","","","",""))
-        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
-            urllib.parse.urlunsplit((b"http", "www.python.org","","",""))
-        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
-            urllib.parse.urlunsplit(("http", b"www.python.org","","",""))
-        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
-            urllib.parse.urljoin("http://python.org", b"http://python.org")
-        with self.assertRaisesRegex(TypeError, "Cannot mix str"):
-            urllib.parse.urljoin(b"http://python.org", "http://python.org")
-
-    def _check_result_type(self, str_type, num_args):
-#        num_args = len(str_type._fields)
-        bytes_type = str_type._encoded_counterpart
-        self.assertIs(bytes_type._decoded_counterpart, str_type)
-        str_args = ('',) * num_args
-        bytes_args = (b'',) * num_args
-        str_result = str_type(*str_args)
-        bytes_result = bytes_type(*bytes_args)
-        encoding = 'ascii'
-        errors = 'strict'
-        self.assertEqual(str_result, str_args)
-        self.assertEqual(bytes_result.decode(), str_args)
-        self.assertEqual(bytes_result.decode(), str_result)
-        self.assertEqual(bytes_result.decode(encoding), str_args)
-        self.assertEqual(bytes_result.decode(encoding), str_result)
-        self.assertEqual(bytes_result.decode(encoding, errors), str_args)
-        self.assertEqual(bytes_result.decode(encoding, errors), str_result)
-        self.assertEqual(bytes_result, bytes_args)
-        self.assertEqual(str_result.encode(), bytes_args)
-        self.assertEqual(str_result.encode(), bytes_result)
-        self.assertEqual(str_result.encode(encoding), bytes_args)
-        self.assertEqual(str_result.encode(encoding), bytes_result)
-        self.assertEqual(str_result.encode(encoding, errors), bytes_args)
-        self.assertEqual(str_result.encode(encoding, errors), bytes_result)
-
-    def test_result_pairs(self):
-        # Check encoding and decoding between result pairs
-        result_types = [
-          (urllib.parse.DefragResult, 2),
-          (urllib.parse.SplitResult, 5),
-          (urllib.parse.ParseResult, 6),
-        ]
-        for result_type in result_types:
-            self._check_result_type(*result_type)
-
-    def _test_parse_qs_encoding(self):
-        result = urllib.parse.parse_qs("key=\u0141%E9", encoding="latin-1")
-        self.assertEqual(result, {'key': ['\u0141\xE9']})
-        result = urllib.parse.parse_qs("key=\u0141%C3%A9", encoding="utf-8")
-        self.assertEqual(result, {'key': ['\u0141\xE9']})
-        result = urllib.parse.parse_qs("key=\u0141%C3%A9", encoding="ascii")
-        self.assertEqual(result, {'key': ['\u0141\ufffd\ufffd']})
-        result = urllib.parse.parse_qs("key=\u0141%E9-", encoding="ascii")
-        self.assertEqual(result, {'key': ['\u0141\ufffd-']})
-        result = urllib.parse.parse_qs("key=\u0141%E9-", encoding="ascii",
-                                                          errors="ignore")
-        self.assertEqual(result, {'key': ['\u0141-']})
-
-    def _test_parse_qsl_encoding(self):
-        result = urllib.parse.parse_qsl("key=\u0141%E9", encoding="latin-1")
-        self.assertEqual(result, [('key', '\u0141\xE9')])
-        result = urllib.parse.parse_qsl("key=\u0141%C3%A9", encoding="utf-8")
-        self.assertEqual(result, [('key', '\u0141\xE9')])
-        result = urllib.parse.parse_qsl("key=\u0141%C3%A9", encoding="ascii")
-        self.assertEqual(result, [('key', '\u0141\ufffd\ufffd')])
-        result = urllib.parse.parse_qsl("key=\u0141%E9-", encoding="ascii")
-        self.assertEqual(result, [('key', '\u0141\ufffd-')])
-        result = urllib.parse.parse_qsl("key=\u0141%E9-", encoding="ascii",
-                                                          errors="ignore")
-        self.assertEqual(result, [('key', '\u0141-')])
-
-    def test_splitnport(self):
-        # Normal cases are exercised by other tests; ensure that we also
-        # catch cases with no port specified. (testcase ensuring coverage)
-        result = urllib.parse.splitnport('parrot:88')
-        self.assertEqual(result, ('parrot', 88))
-        result = urllib.parse.splitnport('parrot')
-        self.assertEqual(result, ('parrot', -1))
-        result = urllib.parse.splitnport('parrot', 55)
-        self.assertEqual(result, ('parrot', 55))
-        result = urllib.parse.splitnport('parrot:')
-        self.assertEqual(result, ('parrot', None))
-
-    def test_splitquery(self):
-        # Normal cases are exercised by other tests; ensure that we also
-        # catch cases with no port specified (testcase ensuring coverage)
-        result = urllib.parse.splitquery('http://python.org/fake?foo=bar')
-        self.assertEqual(result, ('http://python.org/fake', 'foo=bar'))
-        result = urllib.parse.splitquery('http://python.org/fake?foo=bar?')
-        self.assertEqual(result, ('http://python.org/fake?foo=bar', ''))
-        result = urllib.parse.splitquery('http://python.org/fake')
-        self.assertEqual(result, ('http://python.org/fake', None))
-
-    def test_splitvalue(self):
-        # Normal cases are exercised by other tests; test pathological cases
-        # with no key/value pairs. (testcase ensuring coverage)
-        result = urllib.parse.splitvalue('foo=bar')
-        self.assertEqual(result, ('foo', 'bar'))
-        result = urllib.parse.splitvalue('foo=')
-        self.assertEqual(result, ('foo', ''))
-        result = urllib.parse.splitvalue('foobar')
-        self.assertEqual(result, ('foobar', None))
-
-    def test_to_bytes(self):
-        result = urllib.parse.to_bytes('http://www.python.org')
-        self.assertEqual(result, 'http://www.python.org')
-#        self.assertRaises(UnicodeError, urllib.parse.to_bytes,
-#                          'http://www.python.org/medi\u00e6val')
-
-    def test_urlencode_sequences(self):
-        # Other tests incidentally urlencode things; test non-covered cases:
-        # Sequence and object values.
-        result = urllib.parse.urlencode({'a': [1, 2], 'b': (3, 4, 5)}, True)
-        # we cannot rely on ordering here
-        assert set(result.split('&')) == {'a=1', 'a=2', 'b=3', 'b=4', 'b=5'}
-
-        class Trivial:
-            def __str__(self):
-                return 'trivial'
-
-        result = urllib.parse.urlencode({'a': Trivial()}, True)
-        self.assertEqual(result, 'a=trivial')
-
-    def test_quote_from_bytes(self):
-        self.assertRaises(TypeError, urllib.parse.quote_from_bytes, 'foo')
-        result = urllib.parse.quote_from_bytes(b'archaeological arcana')
-        self.assertEqual(result, 'archaeological%20arcana')
-        result = urllib.parse.quote_from_bytes(b'')
-        self.assertEqual(result, '')
-
-    def test_unquote_to_bytes(self):
-        result = urllib.parse.unquote_to_bytes('abc%20def')
-        self.assertEqual(result, b'abc def')
-        result = urllib.parse.unquote_to_bytes('')
-        self.assertEqual(result, b'')
-
-    def test_quote_errors(self):
-        self.assertRaises(TypeError, urllib.parse.quote, b'foo',
-                          encoding='utf-8')
-        self.assertRaises(TypeError, urllib.parse.quote, b'foo', errors='strict')
-
-    def test_issue14072(self):
-        p1 = urllib.parse.urlsplit('tel:+31-641044153')
-        self.assertEqual(p1.scheme, 'tel')
-        self.assertEqual(p1.path, '+31-641044153')
-        p2 = urllib.parse.urlsplit('tel:+31641044153')
-        self.assertEqual(p2.scheme, 'tel')
-        self.assertEqual(p2.path, '+31641044153')
-        # assert the behavior for urlparse
-        p1 = urllib.parse.urlparse('tel:+31-641044153')
-        self.assertEqual(p1.scheme, 'tel')
-        self.assertEqual(p1.path, '+31-641044153')
-        p2 = urllib.parse.urlparse('tel:+31641044153')
-        self.assertEqual(p2.scheme, 'tel')
-        self.assertEqual(p2.path, '+31641044153')
-
-    def test_telurl_params(self):
-        p1 = urllib.parse.urlparse('tel:123-4;phone-context=+1-650-516')
-        self.assertEqual(p1.scheme, 'tel')
-        self.assertEqual(p1.path, '123-4')
-        self.assertEqual(p1.params, 'phone-context=+1-650-516')
-
-        p1 = urllib.parse.urlparse('tel:+1-201-555-0123')
-        self.assertEqual(p1.scheme, 'tel')
-        self.assertEqual(p1.path, '+1-201-555-0123')
-        self.assertEqual(p1.params, '')
-
-        p1 = urllib.parse.urlparse('tel:7042;phone-context=example.com')
-        self.assertEqual(p1.scheme, 'tel')
-        self.assertEqual(p1.path, '7042')
-        self.assertEqual(p1.params, 'phone-context=example.com')
-
-        p1 = urllib.parse.urlparse('tel:863-1234;phone-context=+1-914-555')
-        self.assertEqual(p1.scheme, 'tel')
-        self.assertEqual(p1.path, '863-1234')
-        self.assertEqual(p1.params, 'phone-context=+1-914-555')
-
-
-def test_main():
-    support.run_unittest(UrlParseTestCase)
-
-if __name__ == "__main__":
-    test_main()
diff --git a/urllib.parse/urllib/parse.py b/urllib.parse/urllib/parse.py
deleted file mode 100644
index 8e65d2bf0..000000000
--- a/urllib.parse/urllib/parse.py
+++ /dev/null
@@ -1,974 +0,0 @@
-"""Parse (absolute and relative) URLs.
-
-urlparse module is based upon the following RFC specifications.
-
-RFC 3986 (STD66): "Uniform Resource Identifiers" by T. Berners-Lee, R. Fielding
-and L.  Masinter, January 2005.
-
-RFC 2732 : "Format for Literal IPv6 Addresses in URL's by R.Hinden, B.Carpenter
-and L.Masinter, December 1999.
-
-RFC 2396:  "Uniform Resource Identifiers (URI)": Generic Syntax by T.
-Berners-Lee, R. Fielding, and L. Masinter, August 1998.
-
-RFC 2368: "The mailto URL scheme", by P.Hoffman , L Masinter, J. Zawinski, July 1998.
-
-RFC 1808: "Relative Uniform Resource Locators", by R. Fielding, UC Irvine, June
-1995.
-
-RFC 1738: "Uniform Resource Locators (URL)" by T. Berners-Lee, L. Masinter, M.
-McCahill, December 1994
-
-RFC 3986 is considered the current standard and any future changes to
-urlparse module should conform with it.  The urlparse module is
-currently not entirely compliant with this RFC due to defacto
-scenarios for parsing, and for backward compatibility purposes, some
-parsing quirks from older RFCs are retained. The testcases in
-test_urlparse.py provides a good indicator of parsing behavior.
-"""
-
-import re
-import sys
-import collections
-
-__all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag",
-           "urlsplit", "urlunsplit", "urlencode", "parse_qs",
-           "parse_qsl", "quote", "quote_plus", "quote_from_bytes",
-           "unquote", "unquote_plus", "unquote_to_bytes"]
-
-# A classification of schemes ('' means apply by default)
-uses_relative = ['ftp', 'http', 'gopher', 'nntp', 'imap',
-                 'wais', 'file', 'https', 'shttp', 'mms',
-                 'prospero', 'rtsp', 'rtspu', '', 'sftp',
-                 'svn', 'svn+ssh']
-uses_netloc = ['ftp', 'http', 'gopher', 'nntp', 'telnet',
-               'imap', 'wais', 'file', 'mms', 'https', 'shttp',
-               'snews', 'prospero', 'rtsp', 'rtspu', 'rsync', '',
-               'svn', 'svn+ssh', 'sftp', 'nfs', 'git', 'git+ssh']
-uses_params = ['ftp', 'hdl', 'prospero', 'http', 'imap',
-               'https', 'shttp', 'rtsp', 'rtspu', 'sip', 'sips',
-               'mms', '', 'sftp', 'tel']
-
-# These are not actually used anymore, but should stay for backwards
-# compatibility.  (They are undocumented, but have a public-looking name.)
-non_hierarchical = ['gopher', 'hdl', 'mailto', 'news',
-                    'telnet', 'wais', 'imap', 'snews', 'sip', 'sips']
-uses_query = ['http', 'wais', 'imap', 'https', 'shttp', 'mms',
-              'gopher', 'rtsp', 'rtspu', 'sip', 'sips', '']
-uses_fragment = ['ftp', 'hdl', 'http', 'gopher', 'news',
-                 'nntp', 'wais', 'https', 'shttp', 'snews',
-                 'file', 'prospero', '']
-
-# Characters valid in scheme names
-scheme_chars = ('abcdefghijklmnopqrstuvwxyz'
-                'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
-                '0123456789'
-                '+-.')
-
-# XXX: Consider replacing with functools.lru_cache
-MAX_CACHE_SIZE = 20
-_parse_cache = {}
-
-def clear_cache():
-    """Clear the parse cache and the quoters cache."""
-    _parse_cache.clear()
-    _safe_quoters.clear()
-
-
-# Helpers for bytes handling
-# For 3.2, we deliberately require applications that
-# handle improperly quoted URLs to do their own
-# decoding and encoding. If valid use cases are
-# presented, we may relax this by using latin-1
-# decoding internally for 3.3
-_implicit_encoding = 'ascii'
-_implicit_errors = 'strict'
-
-def _noop(obj):
-    return obj
-
-def _encode_result(obj, encoding=_implicit_encoding,
-                        errors=_implicit_errors):
-    return obj.encode(encoding, errors)
-
-def _decode_args(args, encoding=_implicit_encoding,
-                       errors=_implicit_errors):
-    return tuple(x.decode(encoding, errors) if x else '' for x in args)
-
-def _coerce_args(*args):
-    # Invokes decode if necessary to create str args
-    # and returns the coerced inputs along with
-    # an appropriate result coercion function
-    #   - noop for str inputs
-    #   - encoding function otherwise
-    str_input = isinstance(args[0], str)
-    for arg in args[1:]:
-        # We special-case the empty string to support the
-        # "scheme=''" default argument to some functions
-        if arg and isinstance(arg, str) != str_input:
-            raise TypeError("Cannot mix str and non-str arguments")
-    if str_input:
-        return args + (_noop,)
-    return _decode_args(args) + (_encode_result,)
-
-# Result objects are more helpful than simple tuples
-class _ResultMixinStr(object):
-    """Standard approach to encoding parsed results from str to bytes"""
-    __slots__ = ()
-
-    def encode(self, encoding='ascii', errors='strict'):
-        return self._encoded_counterpart(*(x.encode(encoding, errors) for x in self))
-
-
-class _ResultMixinBytes(object):
-    """Standard approach to decoding parsed results from bytes to str"""
-    __slots__ = ()
-
-    def decode(self, encoding='ascii', errors='strict'):
-        return self._decoded_counterpart(*(x.decode(encoding, errors) for x in self))
-
-
-class _NetlocResultMixinBase(object):
-    """Shared methods for the parsed result objects containing a netloc element"""
-    __slots__ = ()
-
-    @property
-    def username(self):
-        return self._userinfo[0]
-
-    @property
-    def password(self):
-        return self._userinfo[1]
-
-    @property
-    def hostname(self):
-        hostname = self._hostinfo[0]
-        if not hostname:
-            hostname = None
-        elif hostname is not None:
-            hostname = hostname.lower()
-        return hostname
-
-    @property
-    def port(self):
-        port = self._hostinfo[1]
-        if port is not None:
-            port = int(port, 10)
-            # Return None on an illegal port
-            if not ( 0 <= port <= 65535):
-                return None
-        return port
-
-
-class _NetlocResultMixinStr(_NetlocResultMixinBase, _ResultMixinStr):
-    __slots__ = ()
-
-    @property
-    def _userinfo(self):
-        netloc = self.netloc
-        userinfo, have_info, hostinfo = netloc.rpartition('@')
-        if have_info:
-            username, have_password, password = userinfo.partition(':')
-            if not have_password:
-                password = None
-        else:
-            username = password = None
-        return username, password
-
-    @property
-    def _hostinfo(self):
-        netloc = self.netloc
-        _, _, hostinfo = netloc.rpartition('@')
-        _, have_open_br, bracketed = hostinfo.partition('[')
-        if have_open_br:
-            hostname, _, port = bracketed.partition(']')
-            _, have_port, port = port.partition(':')
-        else:
-            hostname, have_port, port = hostinfo.partition(':')
-        if not have_port:
-            port = None
-        return hostname, port
-
-
-class _NetlocResultMixinBytes(_NetlocResultMixinBase, _ResultMixinBytes):
-    __slots__ = ()
-
-    @property
-    def _userinfo(self):
-        netloc = self.netloc
-        userinfo, have_info, hostinfo = netloc.rpartition(b'@')
-        if have_info:
-            username, have_password, password = userinfo.partition(b':')
-            if not have_password:
-                password = None
-        else:
-            username = password = None
-        return username, password
-
-    @property
-    def _hostinfo(self):
-        netloc = self.netloc
-        _, _, hostinfo = netloc.rpartition(b'@')
-        _, have_open_br, bracketed = hostinfo.partition(b'[')
-        if have_open_br:
-            hostname, _, port = bracketed.partition(b']')
-            _, have_port, port = port.partition(b':')
-        else:
-            hostname, have_port, port = hostinfo.partition(b':')
-        if not have_port:
-            port = None
-        return hostname, port
-
-
-from collections import namedtuple
-
-_DefragResultBase = namedtuple('DefragResult', 'url fragment')
-_SplitResultBase = namedtuple('SplitResult', 'scheme netloc path query fragment')
-_ParseResultBase = namedtuple('ParseResult', 'scheme netloc path params query fragment')
-
-# For backwards compatibility, alias _NetlocResultMixinStr
-# ResultBase is no longer part of the documented API, but it is
-# retained since deprecating it isn't worth the hassle
-ResultBase = _NetlocResultMixinStr
-
-# Structured result objects for string data
-class DefragResult(_DefragResultBase, _ResultMixinStr):
-    __slots__ = ()
-    def geturl(self):
-        if self.fragment:
-            return self.url + '#' + self.fragment
-        else:
-            return self.url
-
-class SplitResult(_SplitResultBase, _NetlocResultMixinStr):
-    __slots__ = ()
-    def geturl(self):
-        return urlunsplit(self)
-
-class ParseResult(_ParseResultBase, _NetlocResultMixinStr):
-    __slots__ = ()
-    def geturl(self):
-        return urlunparse(self)
-
-# Structured result objects for bytes data
-class DefragResultBytes(_DefragResultBase, _ResultMixinBytes):
-    __slots__ = ()
-    def geturl(self):
-        if self.fragment:
-            return self.url + b'#' + self.fragment
-        else:
-            return self.url
-
-class SplitResultBytes(_SplitResultBase, _NetlocResultMixinBytes):
-    __slots__ = ()
-    def geturl(self):
-        return urlunsplit(self)
-
-class ParseResultBytes(_ParseResultBase, _NetlocResultMixinBytes):
-    __slots__ = ()
-    def geturl(self):
-        return urlunparse(self)
-
-# Set up the encode/decode result pairs
-def _fix_result_transcoding():
-    _result_pairs = (
-        (DefragResult, DefragResultBytes),
-        (SplitResult, SplitResultBytes),
-        (ParseResult, ParseResultBytes),
-    )
-    for _decoded, _encoded in _result_pairs:
-        _decoded._encoded_counterpart = _encoded
-        _encoded._decoded_counterpart = _decoded
-
-_fix_result_transcoding()
-del _fix_result_transcoding
-
-def urlparse(url, scheme='', allow_fragments=True):
-    """Parse a URL into 6 components:
-    <scheme>://<netloc>/<path>;<params>?<query>#<fragment>
-    Return a 6-tuple: (scheme, netloc, path, params, query, fragment).
-    Note that we don't break the components up in smaller bits
-    (e.g. netloc is a single string) and we don't expand % escapes."""
-    url, scheme, _coerce_result = _coerce_args(url, scheme)
-    splitresult = urlsplit(url, scheme, allow_fragments)
-    scheme, netloc, url, query, fragment = splitresult
-    if scheme in uses_params and ';' in url:
-        url, params = _splitparams(url)
-    else:
-        params = ''
-    result = ParseResult(scheme, netloc, url, params, query, fragment)
-    return _coerce_result(result)
-
-def _splitparams(url):
-    if '/'  in url:
-        i = url.find(';', url.rfind('/'))
-        if i < 0:
-            return url, ''
-    else:
-        i = url.find(';')
-    return url[:i], url[i+1:]
-
-def _splitnetloc(url, start=0):
-    delim = len(url)   # position of end of domain part of url, default is end
-    for c in '/?#':    # look for delimiters; the order is NOT important
-        wdelim = url.find(c, start)        # find first of this delim
-        if wdelim >= 0:                    # if found
-            delim = min(delim, wdelim)     # use earliest delim position
-    return url[start:delim], url[delim:]   # return (domain, rest)
-
-def urlsplit(url, scheme='', allow_fragments=True):
-    """Parse a URL into 5 components:
-    <scheme>://<netloc>/<path>?<query>#<fragment>
-    Return a 5-tuple: (scheme, netloc, path, query, fragment).
-    Note that we don't break the components up in smaller bits
-    (e.g. netloc is a single string) and we don't expand % escapes."""
-    url, scheme, _coerce_result = _coerce_args(url, scheme)
-    allow_fragments = bool(allow_fragments)
-    key = url, scheme, allow_fragments, type(url), type(scheme)
-    cached = _parse_cache.get(key, None)
-    if cached:
-        return _coerce_result(cached)
-    if len(_parse_cache) >= MAX_CACHE_SIZE: # avoid runaway growth
-        clear_cache()
-    netloc = query = fragment = ''
-    i = url.find(':')
-    if i > 0:
-        if url[:i] == 'http': # optimize the common case
-            scheme = url[:i].lower()
-            url = url[i+1:]
-            if url[:2] == '//':
-                netloc, url = _splitnetloc(url, 2)
-                if (('[' in netloc and ']' not in netloc) or
-                        (']' in netloc and '[' not in netloc)):
-                    raise ValueError("Invalid IPv6 URL")
-            if allow_fragments and '#' in url:
-                url, fragment = url.split('#', 1)
-            if '?' in url:
-                url, query = url.split('?', 1)
-            v = SplitResult(scheme, netloc, url, query, fragment)
-            _parse_cache[key] = v
-            return _coerce_result(v)
-        for c in url[:i]:
-            if c not in scheme_chars:
-                break
-        else:
-            # make sure "url" is not actually a port number (in which case
-            # "scheme" is really part of the path)
-            rest = url[i+1:]
-            if not rest or any(c not in '0123456789' for c in rest):
-                # not a port number
-                scheme, url = url[:i].lower(), rest
-
-    if url[:2] == '//':
-        netloc, url = _splitnetloc(url, 2)
-        if (('[' in netloc and ']' not in netloc) or
-                (']' in netloc and '[' not in netloc)):
-            raise ValueError("Invalid IPv6 URL")
-    if allow_fragments and '#' in url:
-        url, fragment = url.split('#', 1)
-    if '?' in url:
-        url, query = url.split('?', 1)
-    v = SplitResult(scheme, netloc, url, query, fragment)
-    _parse_cache[key] = v
-    return _coerce_result(v)
-
-def urlunparse(components):
-    """Put a parsed URL back together again.  This may result in a
-    slightly different, but equivalent URL, if the URL that was parsed
-    originally had redundant delimiters, e.g. a ? with an empty query
-    (the draft states that these are equivalent)."""
-    scheme, netloc, url, params, query, fragment, _coerce_result = (
-                                                  _coerce_args(*components))
-    if params:
-        url = "%s;%s" % (url, params)
-    return _coerce_result(urlunsplit((scheme, netloc, url, query, fragment)))
-
-def urlunsplit(components):
-    """Combine the elements of a tuple as returned by urlsplit() into a
-    complete URL as a string. The data argument can be any five-item iterable.
-    This may result in a slightly different, but equivalent URL, if the URL that
-    was parsed originally had unnecessary delimiters (for example, a ? with an
-    empty query; the RFC states that these are equivalent)."""
-    scheme, netloc, url, query, fragment, _coerce_result = (
-                                          _coerce_args(*components))
-    if netloc or (scheme and scheme in uses_netloc and url[:2] != '//'):
-        if url and url[:1] != '/': url = '/' + url
-        url = '//' + (netloc or '') + url
-    if scheme:
-        url = scheme + ':' + url
-    if query:
-        url = url + '?' + query
-    if fragment:
-        url = url + '#' + fragment
-    return _coerce_result(url)
-
-def urljoin(base, url, allow_fragments=True):
-    """Join a base URL and a possibly relative URL to form an absolute
-    interpretation of the latter."""
-    if not base:
-        return url
-    if not url:
-        return base
-    base, url, _coerce_result = _coerce_args(base, url)
-    bscheme, bnetloc, bpath, bparams, bquery, bfragment = \
-            urlparse(base, '', allow_fragments)
-    scheme, netloc, path, params, query, fragment = \
-            urlparse(url, bscheme, allow_fragments)
-    if scheme != bscheme or scheme not in uses_relative:
-        return _coerce_result(url)
-    if scheme in uses_netloc:
-        if netloc:
-            return _coerce_result(urlunparse((scheme, netloc, path,
-                                              params, query, fragment)))
-        netloc = bnetloc
-    if path[:1] == '/':
-        return _coerce_result(urlunparse((scheme, netloc, path,
-                                          params, query, fragment)))
-    if not path and not params:
-        path = bpath
-        params = bparams
-        if not query:
-            query = bquery
-        return _coerce_result(urlunparse((scheme, netloc, path,
-                                          params, query, fragment)))
-    segments = bpath.split('/')[:-1] + path.split('/')
-    # XXX The stuff below is bogus in various ways...
-    if segments[-1] == '.':
-        segments[-1] = ''
-    while '.' in segments:
-        segments.remove('.')
-    while 1:
-        i = 1
-        n = len(segments) - 1
-        while i < n:
-            if (segments[i] == '..'
-                and segments[i-1] not in ('', '..')):
-                del segments[i-1:i+1]
-                break
-            i = i+1
-        else:
-            break
-    if segments == ['', '..']:
-        segments[-1] = ''
-    elif len(segments) >= 2 and segments[-1] == '..':
-        segments[-2:] = ['']
-    return _coerce_result(urlunparse((scheme, netloc, '/'.join(segments),
-                                      params, query, fragment)))
-
-def urldefrag(url):
-    """Removes any existing fragment from URL.
-
-    Returns a tuple of the defragmented URL and the fragment.  If
-    the URL contained no fragments, the second element is the
-    empty string.
-    """
-    url, _coerce_result = _coerce_args(url)
-    if '#' in url:
-        s, n, p, a, q, frag = urlparse(url)
-        defrag = urlunparse((s, n, p, a, q, ''))
-    else:
-        frag = ''
-        defrag = url
-    return _coerce_result(DefragResult(defrag, frag))
-
-_hexdig = '0123456789ABCDEFabcdef'
-_hextobyte = {(a + b).encode(): bytes([int(a + b, 16)])
-              for a in _hexdig for b in _hexdig}
-
-def unquote_to_bytes(string):
-    """unquote_to_bytes('abc%20def') -> b'abc def'."""
-    # Note: strings are encoded as UTF-8. This is only an issue if it contains
-    # unescaped non-ASCII characters, which URIs should not.
-    if not string:
-        # Is it a string-like object?
-        string.split
-        return b''
-    if isinstance(string, str):
-        string = string.encode('utf-8')
-    bits = string.split(b'%')
-    if len(bits) == 1:
-        return string
-    res = [bits[0]]
-    append = res.append
-    for item in bits[1:]:
-        try:
-            append(_hextobyte[item[:2]])
-            append(item[2:])
-        except KeyError:
-            append(b'%')
-            append(item)
-    return b''.join(res)
-
-_asciire = re.compile(r'([\x00-\x7f]+)')
-
-def unquote(string, encoding='utf-8', errors='replace'):
-    """Replace %xx escapes by their single-character equivalent. The optional
-    encoding and errors parameters specify how to decode percent-encoded
-    sequences into Unicode characters, as accepted by the bytes.decode()
-    method.
-    By default, percent-encoded sequences are decoded with UTF-8, and invalid
-    sequences are replaced by a placeholder character.
-
-    unquote('abc%20def') -> 'abc def'.
-    """
-    if '%' not in string:
-        string.split
-        return string
-    if encoding is None:
-        encoding = 'utf-8'
-    if errors is None:
-        errors = 'replace'
-    bits = _asciire.split(string)
-    res = [bits[0]]
-    append = res.append
-    for i in range(1, len(bits), 2):
-        append(unquote_to_bytes(bits[i]).decode(encoding, errors))
-        append(bits[i + 1])
-    return ''.join(res)
-
-def parse_qs(qs, keep_blank_values=False, strict_parsing=False,
-             encoding='utf-8', errors='replace'):
-    """Parse a query given as a string argument.
-
-        Arguments:
-
-        qs: percent-encoded query string to be parsed
-
-        keep_blank_values: flag indicating whether blank values in
-            percent-encoded queries should be treated as blank strings.
-            A true value indicates that blanks should be retained as
-            blank strings.  The default false value indicates that
-            blank values are to be ignored and treated as if they were
-            not included.
-
-        strict_parsing: flag indicating what to do with parsing errors.
-            If false (the default), errors are silently ignored.
-            If true, errors raise a ValueError exception.
-
-        encoding and errors: specify how to decode percent-encoded sequences
-            into Unicode characters, as accepted by the bytes.decode() method.
-    """
-    parsed_result = {}
-    pairs = parse_qsl(qs, keep_blank_values, strict_parsing,
-                      encoding=encoding, errors=errors)
-    for name, value in pairs:
-        if name in parsed_result:
-            parsed_result[name].append(value)
-        else:
-            parsed_result[name] = [value]
-    return parsed_result
-
-def parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
-              encoding='utf-8', errors='replace'):
-    """Parse a query given as a string argument.
-
-    Arguments:
-
-    qs: percent-encoded query string to be parsed
-
-    keep_blank_values: flag indicating whether blank values in
-        percent-encoded queries should be treated as blank strings.  A
-        true value indicates that blanks should be retained as blank
-        strings.  The default false value indicates that blank values
-        are to be ignored and treated as if they were  not included.
-
-    strict_parsing: flag indicating what to do with parsing errors. If
-        false (the default), errors are silently ignored. If true,
-        errors raise a ValueError exception.
-
-    encoding and errors: specify how to decode percent-encoded sequences
-        into Unicode characters, as accepted by the bytes.decode() method.
-
-    Returns a list, as G-d intended.
-    """
-    qs, _coerce_result = _coerce_args(qs)
-    pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
-    r = []
-    for name_value in pairs:
-        if not name_value and not strict_parsing:
-            continue
-        nv = name_value.split('=', 1)
-        if len(nv) != 2:
-            if strict_parsing:
-                raise ValueError("bad query field: %r" % (name_value,))
-            # Handle case of a control-name with no equal sign
-            if keep_blank_values:
-                nv.append('')
-            else:
-                continue
-        if len(nv[1]) or keep_blank_values:
-            name = nv[0].replace('+', ' ')
-            name = unquote(name, encoding=encoding, errors=errors)
-            name = _coerce_result(name)
-            value = nv[1].replace('+', ' ')
-            value = unquote(value, encoding=encoding, errors=errors)
-            value = _coerce_result(value)
-            r.append((name, value))
-    return r
-
-def unquote_plus(string, encoding='utf-8', errors='replace'):
-    """Like unquote(), but also replace plus signs by spaces, as required for
-    unquoting HTML form values.
-
-    unquote_plus('%7e/abc+def') -> '~/abc def'
-    """
-    string = string.replace('+', ' ')
-    return unquote(string, encoding, errors)
-
-_ALWAYS_SAFE = frozenset(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
-                         b'abcdefghijklmnopqrstuvwxyz'
-                         b'0123456789'
-                         b'_.-')
-_ALWAYS_SAFE_BYTES = bytes(_ALWAYS_SAFE)
-_safe_quoters = {}
-
-class Quoter(collections.defaultdict):
-    """A mapping from bytes (in range(0,256)) to strings.
-
-    String values are percent-encoded byte values, unless the key < 128, and
-    in the "safe" set (either the specified safe set, or default set).
-    """
-    # Keeps a cache internally, using defaultdict, for efficiency (lookups
-    # of cached keys don't call Python code at all).
-    def __init__(self, safe):
-        """safe: bytes object."""
-        self.safe = _ALWAYS_SAFE.union(safe)
-
-    def __repr__(self):
-        # Without this, will just display as a defaultdict
-        return "<Quoter %r>" % dict(self)
-
-    def __missing__(self, b):
-        # Handle a cache miss. Store quoted string in cache and return.
-        res = chr(b) if b in self.safe else '%{:02X}'.format(b)
-        self[b] = res
-        return res
-
-def quote(string, safe='/', encoding=None, errors=None):
-    """quote('abc def') -> 'abc%20def'
-
-    Each part of a URL, e.g. the path info, the query, etc., has a
-    different set of reserved characters that must be quoted.
-
-    RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
-    the following reserved characters.
-
-    reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
-                  "$" | ","
-
-    Each of these characters is reserved in some component of a URL,
-    but not necessarily in all of them.
-
-    By default, the quote function is intended for quoting the path
-    section of a URL.  Thus, it will not encode '/'.  This character
-    is reserved, but in typical usage the quote function is being
-    called on a path where the existing slash characters are used as
-    reserved characters.
-
-    string and safe may be either str or bytes objects. encoding must
-    not be specified if string is a str.
-
-    The optional encoding and errors parameters specify how to deal with
-    non-ASCII characters, as accepted by the str.encode method.
-    By default, encoding='utf-8' (characters are encoded with UTF-8), and
-    errors='strict' (unsupported characters raise a UnicodeEncodeError).
-    """
-    if isinstance(string, str):
-        if not string:
-            return string
-        if encoding is None:
-            encoding = 'utf-8'
-        if errors is None:
-            errors = 'strict'
-        string = string.encode(encoding, errors)
-    else:
-        if encoding is not None:
-            raise TypeError("quote() doesn't support 'encoding' for bytes")
-        if errors is not None:
-            raise TypeError("quote() doesn't support 'errors' for bytes")
-    return quote_from_bytes(string, safe)
-
-def quote_plus(string, safe='', encoding=None, errors=None):
-    """Like quote(), but also replace ' ' with '+', as required for quoting
-    HTML form values. Plus signs in the original string are escaped unless
-    they are included in safe. It also does not have safe default to '/'.
-    """
-    # Check if ' ' in string, where string may either be a str or bytes.  If
-    # there are no spaces, the regular quote will produce the right answer.
-    if ((isinstance(string, str) and ' ' not in string) or
-        (isinstance(string, bytes) and b' ' not in string)):
-        return quote(string, safe, encoding, errors)
-    if isinstance(safe, str):
-        space = ' '
-    else:
-        space = b' '
-    string = quote(string, safe + space, encoding, errors)
-    return string.replace(' ', '+')
-
-def quote_from_bytes(bs, safe='/'):
-    """Like quote(), but accepts a bytes object rather than a str, and does
-    not perform string-to-bytes encoding.  It always returns an ASCII string.
-    quote_from_bytes(b'abc def\x3f') -> 'abc%20def%3f'
-    """
-    if not isinstance(bs, (bytes, bytearray)):
-        raise TypeError("quote_from_bytes() expected bytes")
-    if not bs:
-        return ''
-    if isinstance(safe, str):
-        # Normalize 'safe' by converting to bytes and removing non-ASCII chars
-        safe = safe.encode('ascii', 'ignore')
-    else:
-        safe = bytes([c for c in safe if c < 128])
-    if not bs.rstrip(_ALWAYS_SAFE_BYTES + safe):
-        return bs.decode()
-    try:
-        quoter = _safe_quoters[safe]
-    except KeyError:
-        _safe_quoters[safe] = quoter = Quoter(safe).__getitem__
-    return ''.join([quoter(char) for char in bs])
-
-def urlencode(query, doseq=False, safe='', encoding=None, errors=None):
-    """Encode a dict or sequence of two-element tuples into a URL query string.
-
-    If any values in the query arg are sequences and doseq is true, each
-    sequence element is converted to a separate parameter.
-
-    If the query arg is a sequence of two-element tuples, the order of the
-    parameters in the output will match the order of parameters in the
-    input.
-
-    The components of a query arg may each be either a string or a bytes type.
-    When a component is a string, the safe, encoding and error parameters are
-    sent to the quote_plus function for encoding.
-    """
-
-    if hasattr(query, "items"):
-        query = query.items()
-    else:
-        # It's a bother at times that strings and string-like objects are
-        # sequences.
-        try:
-            # non-sequence items should not work with len()
-            # non-empty strings will fail this
-            if len(query) and not isinstance(query[0], tuple):
-                raise TypeError
-            # Zero-length sequences of all types will get here and succeed,
-            # but that's a minor nit.  Since the original implementation
-            # allowed empty dicts that type of behavior probably should be
-            # preserved for consistency
-        except TypeError:
-#            ty, va, tb = sys.exc_info()
-            raise TypeError("not a valid non-string sequence "
-                            "or mapping object")#.with_traceback(tb)
-
-    l = []
-    if not doseq:
-        for k, v in query:
-            if isinstance(k, bytes):
-                k = quote_plus(k, safe)
-            else:
-                k = quote_plus(str(k), safe, encoding, errors)
-
-            if isinstance(v, bytes):
-                v = quote_plus(v, safe)
-            else:
-                v = quote_plus(str(v), safe, encoding, errors)
-            l.append(k + '=' + v)
-    else:
-        for k, v in query:
-            if isinstance(k, bytes):
-                k = quote_plus(k, safe)
-            else:
-                k = quote_plus(str(k), safe, encoding, errors)
-
-            if isinstance(v, bytes):
-                v = quote_plus(v, safe)
-                l.append(k + '=' + v)
-            elif isinstance(v, str):
-                v = quote_plus(v, safe, encoding, errors)
-                l.append(k + '=' + v)
-            else:
-                try:
-                    # Is this a sufficient test for sequence-ness?
-                    x = len(v)
-                except TypeError:
-                    # not a sequence
-                    v = quote_plus(str(v), safe, encoding, errors)
-                    l.append(k + '=' + v)
-                else:
-                    # loop over the sequence
-                    for elt in v:
-                        if isinstance(elt, bytes):
-                            elt = quote_plus(elt, safe)
-                        else:
-                            elt = quote_plus(str(elt), safe, encoding, errors)
-                        l.append(k + '=' + elt)
-    return '&'.join(l)
-
-# Utilities to parse URLs (most of these return None for missing parts):
-# unwrap('<URL:type://host/path>') --> 'type://host/path'
-# splittype('type:opaquestring') --> 'type', 'opaquestring'
-# splithost('//host[:port]/path') --> 'host[:port]', '/path'
-# splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'
-# splitpasswd('user:passwd') -> 'user', 'passwd'
-# splitport('host:port') --> 'host', 'port'
-# splitquery('/path?query') --> '/path', 'query'
-# splittag('/path#tag') --> '/path', 'tag'
-# splitattr('/path;attr1=value1;attr2=value2;...') ->
-#   '/path', ['attr1=value1', 'attr2=value2', ...]
-# splitvalue('attr=value') --> 'attr', 'value'
-# urllib.parse.unquote('abc%20def') -> 'abc def'
-# quote('abc def') -> 'abc%20def')
-
-def to_bytes(url):
-    """to_bytes(u"URL") --> 'URL'."""
-    # Most URL schemes require ASCII. If that changes, the conversion
-    # can be relaxed.
-    # XXX get rid of to_bytes()
-    if isinstance(url, str):
-        try:
-            url = url.encode("ASCII").decode()
-        except UnicodeError:
-            raise UnicodeError("URL " + repr(url) +
-                               " contains non-ASCII characters")
-    return url
-
-def unwrap(url):
-    """unwrap('<URL:type://host/path>') --> 'type://host/path'."""
-    url = str(url).strip()
-    if url[:1] == '<' and url[-1:] == '>':
-        url = url[1:-1].strip()
-    if url[:4] == 'URL:': url = url[4:].strip()
-    return url
-
-_typeprog = None
-def splittype(url):
-    """splittype('type:opaquestring') --> 'type', 'opaquestring'."""
-    global _typeprog
-    if _typeprog is None:
-        import re
-        _typeprog = re.compile('^([^/:]+):')
-
-    match = _typeprog.match(url)
-    if match:
-        scheme = match.group(1)
-        return scheme.lower(), url[len(scheme) + 1:]
-    return None, url
-
-_hostprog = None
-def splithost(url):
-    """splithost('//host[:port]/path') --> 'host[:port]', '/path'."""
-    global _hostprog
-    if _hostprog is None:
-        import re
-        _hostprog = re.compile('^//([^/?]*)(.*)$')
-
-    match = _hostprog.match(url)
-    if match:
-        host_port = match.group(1)
-        path = match.group(2)
-        if path and not path.startswith('/'):
-            path = '/' + path
-        return host_port, path
-    return None, url
-
-_userprog = None
-def splituser(host):
-    """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'."""
-    global _userprog
-    if _userprog is None:
-        import re
-        _userprog = re.compile('^(.*)@(.*)$')
-
-    match = _userprog.match(host)
-    if match: return match.group(1, 2)
-    return None, host
-
-_passwdprog = None
-def splitpasswd(user):
-    """splitpasswd('user:passwd') -> 'user', 'passwd'."""
-    global _passwdprog
-    if _passwdprog is None:
-        import re
-        _passwdprog = re.compile('^([^:]*):(.*)$',re.S)
-
-    match = _passwdprog.match(user)
-    if match: return match.group(1, 2)
-    return user, None
-
-# splittag('/path#tag') --> '/path', 'tag'
-_portprog = None
-def splitport(host):
-    """splitport('host:port') --> 'host', 'port'."""
-    global _portprog
-    if _portprog is None:
-        import re
-        _portprog = re.compile('^(.*):([0-9]+)$')
-
-    match = _portprog.match(host)
-    if match: return match.group(1, 2)
-    return host, None
-
-_nportprog = None
-def splitnport(host, defport=-1):
-    """Split host and port, returning numeric port.
-    Return given default port if no ':' found; defaults to -1.
-    Return numerical port if a valid number are found after ':'.
-    Return None if ':' but not a valid number."""
-    global _nportprog
-    if _nportprog is None:
-        import re
-        _nportprog = re.compile('^(.*):(.*)$')
-
-    match = _nportprog.match(host)
-    if match:
-        host, port = match.group(1, 2)
-        try:
-            if not port: raise ValueError("no digits")
-            nport = int(port)
-        except ValueError:
-            nport = None
-        return host, nport
-    return host, defport
-
-_queryprog = None
-def splitquery(url):
-    """splitquery('/path?query') --> '/path', 'query'."""
-    global _queryprog
-    if _queryprog is None:
-        import re
-        _queryprog = re.compile('^(.*)\?([^?]*)$')
-
-    match = _queryprog.match(url)
-    if match: return match.group(1, 2)
-    return url, None
-
-_tagprog = None
-def splittag(url):
-    """splittag('/path#tag') --> '/path', 'tag'."""
-    global _tagprog
-    if _tagprog is None:
-        import re
-        _tagprog = re.compile('^(.*)#([^#]*)$')
-
-    match = _tagprog.match(url)
-    if match: return match.group(1, 2)
-    return url, None
-
-def splitattr(url):
-    """splitattr('/path;attr1=value1;attr2=value2;...') ->
-        '/path', ['attr1=value1', 'attr2=value2', ...]."""
-    words = url.split(';')
-    return words[0], words[1:]
-
-_valueprog = None
-def splitvalue(attr):
-    """splitvalue('attr=value') --> 'attr', 'value'."""
-    global _valueprog
-    if _valueprog is None:
-        import re
-        _valueprog = re.compile('^([^=]*)=(.*)$')
-
-    match = _valueprog.match(attr)
-    if match: return match.group(1, 2)
-    return attr, None
diff --git a/urllib.urequest/metadata.txt b/urllib.urequest/metadata.txt
deleted file mode 100644
index bef75b554..000000000
--- a/urllib.urequest/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = micropython-lib
-type = package
-version = 0.1
-author = Paul Sokolovsky
diff --git a/urllib.urequest/setup.py b/urllib.urequest/setup.py
deleted file mode 100644
index fc6c137e8..000000000
--- a/urllib.urequest/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-urllib.urequest',
-      version='0.1',
-      description='urllib.urequest module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      packages=['urllib'])
diff --git a/urllib.urequest/urllib/urequest.py b/urllib.urequest/urllib/urequest.py
deleted file mode 100644
index 2e9fd95e6..000000000
--- a/urllib.urequest/urllib/urequest.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import usocket
-
-def urlopen(url, data=None):
-    if data:
-        raise NotImplementedError("POST is not yet supported")
-    try:
-        proto, dummy, host, path = url.split("/", 3)
-    except ValueError:
-        proto, dummy, host = url.split("/", 2)
-        path = ""
-    if proto != "http:":
-        raise ValueError("Unsupported protocol: " + proto)
-
-    ai = usocket.getaddrinfo(host, 80)
-    addr = ai[0][4]
-    s = usocket.socket()
-    s.connect(addr)
-    s.send(b"GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n" % (path, host))
-
-    l = s.readline()
-    protover, status, msg = l.split(None, 2)
-    status = int(status)
-    #print(protover, status, msg)
-    while True:
-        l = s.readline()
-        if not l or l == b"\r\n":
-            break
-        #print(line)
-        if l.startswith(b"Transfer-Encoding:"):
-            if b"chunked" in line:
-                raise ValueError("Unsupported " + l)
-        elif l.startswith(b"Location:"):
-            raise NotImplementedError("Redirects not yet supported")
-
-    return s
diff --git a/urllib/setup.py b/urllib/setup.py
deleted file mode 100644
index 1e2257bd8..000000000
--- a/urllib/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise distutils will peek up our
-# module instead of system one.
-sys.path.pop(0)
-sys.path.insert(0, '..')
-from setuptools import setup
-import metadata
-
-NAME = 'urllib'
-
-setup(name='micropython-' + NAME,
-      version='0.0.0',
-      description=metadata.desc_dummy(NAME),
-      url=metadata.url,
-      author=metadata.author_upy_devels,
-      author_email=metadata.author_upy_devels_email,
-      license='MIT',
-      py_modules=[NAME])
diff --git a/urllib/urllib.py b/urllib/urllib.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/utarfile/example-extract.py b/utarfile/example-extract.py
deleted file mode 100644
index a8f828cc9..000000000
--- a/utarfile/example-extract.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import sys
-import os
-import shutil
-import utarfile
-
-t = utarfile.TarFile(sys.argv[1])
-for i in t:
-    print(i)
-    if i.type == utarfile.DIRTYPE:
-        os.makedirs(i.name)
-    else:
-        f = t.extractfile(i)
-        shutil.copyfileobj(f, open(i.name, "wb"))
diff --git a/utarfile/metadata.txt b/utarfile/metadata.txt
deleted file mode 100644
index 9b5bff0a8..000000000
--- a/utarfile/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1.1
-author = Paul Sokolovsky
-long_desc = Lightweight tarfile module subset
diff --git a/utarfile/setup.py b/utarfile/setup.py
deleted file mode 100644
index 4c1f4d0d3..000000000
--- a/utarfile/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-utarfile',
-      version='0.1.1',
-      description='utarfile module for MicroPython',
-      long_description='Lightweight tarfile module subset',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['utarfile'])
diff --git a/utarfile/utarfile.py b/utarfile/utarfile.py
deleted file mode 100644
index 2c3ab7f50..000000000
--- a/utarfile/utarfile.py
+++ /dev/null
@@ -1,82 +0,0 @@
-import uctypes
-
-# http://www.gnu.org/software/tar/manual/html_node/Standard.html
-TAR_HEADER = {
-    "name": (uctypes.ARRAY | 0, uctypes.UINT8 | 100),
-    "size": (uctypes.ARRAY | 124, uctypes.UINT8 | 12),
-}
-
-DIRTYPE = "dir"
-REGTYPE = "file"
-
-def roundup(val, align):
-    return (val + align - 1) & ~(align - 1)
-
-def skip(f, size):
-    assert size % 512 == 0
-    buf = bytearray(512)
-    while size:
-        size -= f.readinto(buf)
-
-class FileSection:
-
-    def __init__(self, f, content_len, aligned_len):
-        self.f = f
-        self.content_len = content_len
-        self.align = aligned_len - content_len
-
-    def read(self, sz=65536):
-        if self.content_len == 0:
-            return b""
-        if sz > self.content_len:
-            sz = self.content_len
-        data = self.f.read(sz)
-        sz = len(data)
-        self.content_len -= sz
-        return data
-
-    def skip(self):
-        self.f.read(self.content_len + self.align)
-
-class TarInfo:
-
-    def __str__(self):
-        return "TarInfo(%r, %s, %d)" % (self.name, self.type, self.size)
-
-class TarFile:
-
-    def __init__(self, name):
-        self.f = open(name, "rb")
-        self.subf = None
-
-    def next(self):
-            if self.subf:
-                self.subf.skip()
-            buf = self.f.read(512)
-            if not buf:
-                return None
-
-            h = uctypes.struct(uctypes.addressof(buf), TAR_HEADER, uctypes.LITTLE_ENDIAN)
-
-            # Empty block means end of archive
-            if h.name[0] == 0:
-                return None
-
-            d = TarInfo()
-            d.name = str(h.name, "utf-8").rstrip()
-            d.size = int(bytes(h.size).rstrip(), 8)
-            d.type = [REGTYPE, DIRTYPE][d.name[-1] == "/"]
-            self.subf = d.subf = FileSection(self.f, d.size, roundup(d.size, 512))
-            return d
-
-    def __iter__(self):
-        return self
-
-    def __next__(self):
-        v = self.next()
-        if v is None:
-            raise StopIteration
-        return v
-
-    def extractfile(self, tarinfo):
-        return tarinfo.subf
diff --git a/uu/metadata.txt b/uu/metadata.txt
deleted file mode 100644
index b8b19ee32..000000000
--- a/uu/metadata.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-srctype = cpython
-type = module
-version = 0.5
-depends = binascii, os
diff --git a/uu/setup.py b/uu/setup.py
deleted file mode 100644
index 93c1d3ec3..000000000
--- a/uu/setup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-uu',
-      version='0.5',
-      description='CPython uu module ported to MicroPython',
-      long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='CPython Developers',
-      author_email='python-dev@python.org',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='Python',
-      py_modules=['uu'],
-      install_requires=['micropython-binascii', 'micropython-os'])
diff --git a/uuid/metadata.txt b/uuid/metadata.txt
deleted file mode 100644
index 357bcc3b5..000000000
--- a/uuid/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.0
diff --git a/uuid/setup.py b/uuid/setup.py
deleted file mode 100644
index e79cfa9d2..000000000
--- a/uuid/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-uuid',
-      version='0.0.0',
-      description='Dummy uuid module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['uuid'])
diff --git a/uuid/uuid.py b/uuid/uuid.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/warnings/example_warn.py b/warnings/example_warn.py
deleted file mode 100644
index fb032971c..000000000
--- a/warnings/example_warn.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import warnings
-
-warnings.warn('block_size of %d seems too small; using our '
-                               'default of %d.',
-                               RuntimeError, 2)
-#                               RuntimeWarning, 2)
diff --git a/warnings/metadata.txt b/warnings/metadata.txt
deleted file mode 100644
index 6b5dc7328..000000000
--- a/warnings/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=micropython-lib
-type=module
-version=0.1
diff --git a/warnings/setup.py b/warnings/setup.py
deleted file mode 100644
index 2b51f54cf..000000000
--- a/warnings/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-warnings',
-      version='0.1',
-      description='warnings module for MicroPython',
-      long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['warnings'])
diff --git a/weakref/metadata.txt b/weakref/metadata.txt
deleted file mode 100644
index 34e7b20b2..000000000
--- a/weakref/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype=dummy
-type=module
-version=0.0.1
diff --git a/weakref/setup.py b/weakref/setup.py
deleted file mode 100644
index 8366c09bf..000000000
--- a/weakref/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-weakref',
-      version='0.0.1',
-      description='Dummy weakref module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['weakref'])
diff --git a/weakref/weakref.py b/weakref/weakref.py
deleted file mode 100644
index 76aabfa31..000000000
--- a/weakref/weakref.py
+++ /dev/null
@@ -1,7 +0,0 @@
-#
-# This is completely dummy implementation, which does not
-# provide real weak references, and thus will hoard memory!
-#
-
-def proxy(obj, cb=None):
-    return obj
diff --git a/xmltok/metadata.txt b/xmltok/metadata.txt
deleted file mode 100644
index 9db09d96a..000000000
--- a/xmltok/metadata.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-srctype = micropython-lib
-type = module
-version = 0.1
-author = Paul Sokolovsky
-long_desc = Simple XML tokenizer
diff --git a/xmltok/setup.py b/xmltok/setup.py
deleted file mode 100644
index 6ea8a1a23..000000000
--- a/xmltok/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-xmltok',
-      version='0.1',
-      description='xmltok module for MicroPython',
-      long_description='Simple XML tokenizer',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='Paul Sokolovsky',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['xmltok'])
diff --git a/xmltok/test_xmltok.py b/xmltok/test_xmltok.py
deleted file mode 100644
index 05953e909..000000000
--- a/xmltok/test_xmltok.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import xmltok
-
-expected = [
-('PI', 'xml'),
-('ATTR', ('', 'version'), '1.0'),
-('START_TAG', ('s', 'Envelope')),
-('ATTR', ('xmlns', 's'), 'http://schemas.xmlsoap.org/soap/envelope/'),
-('ATTR', ('s', 'encodingStyle'), 'http://schemas.xmlsoap.org/soap/encoding/'),
-('START_TAG', ('s', 'Body')),
-('START_TAG', ('u', 'GetConnectionTypeInfo')),
-('ATTR', ('xmlns', 'u'), 'urn:schemas-upnp-org:service:WANIPConnection:1'),
-('TEXT', 'foo bar\n  baz\n  \n'),
-('END_TAG', ('u', 'GetConnectionTypeInfo')),
-('END_TAG', ('s', 'Body')),
-('END_TAG', ('s', 'Envelope')),
-]
-
-ex = iter(expected)
-for i in xmltok.tokenize(open("test.xml")):
-    #print(i)
-    assert i == next(ex)
diff --git a/xmltok/xmltok.py b/xmltok/xmltok.py
deleted file mode 100644
index c71437874..000000000
--- a/xmltok/xmltok.py
+++ /dev/null
@@ -1,134 +0,0 @@
-TEXT = "TEXT"
-START_TAG = "START_TAG"
-#START_TAG_DONE = "START_TAG_DONE"
-END_TAG = "END_TAG"
-PI = "PI"
-#PI_DONE = "PI_DONE"
-ATTR = "ATTR"
-#ATTR_VAL = "ATTR_VAL"
-
-class XMLSyntaxError(Exception):
-    pass
-
-class XMLTokenizer:
-
-    def __init__(self, f):
-        self.f = f
-        self.nextch()
-
-    def curch(self):
-        return self.c
-
-    def getch(self):
-        c = self.c
-        self.nextch()
-        return c
-
-    def eof(self):
-        return self.c == ""
-
-    def nextch(self):
-        self.c = self.f.read(1)
-        if not self.c:
-            raise StopIteration
-        return self.c
-
-    def skip_ws(self):
-        while self.curch().isspace():
-            self.nextch()
-
-    def isident(self):
-        self.skip_ws()
-        return self.curch().isalpha()
-
-    def getident(self):
-        self.skip_ws()
-        ident = ""
-        while True:
-            c = self.curch()
-            if not(c.isalpha() or c.isdigit() or c in "_-."):
-                break
-            ident += self.getch()
-        return ident
-
-    def getnsident(self):
-        ns = ""
-        ident = self.getident()
-        if self.curch() == ":":
-            self.nextch()
-            ns = ident
-            ident = self.getident()
-        return (ns, ident)
-
-    def match(self, c):
-        self.skip_ws()
-        if self.curch() == c:
-            self.nextch()
-            return True
-        return False
-
-    def expect(self, c):
-        if not self.match(c):
-            raise XMLSyntaxError
-
-    def lex_attrs_till(self):
-        while self.isident():
-            attr = self.getnsident()
-            #yield (ATTR, attr)
-            self.expect("=")
-            self.expect('"')
-            val = ""
-            while self.curch() != '"':
-                val += self.getch()
-            #yield (ATTR_VAL, val)
-            self.expect('"')
-            yield (ATTR, attr, val)
-
-    def tokenize(self):
-        while not self.eof():
-            if self.match("<"):
-                if self.match("/"):
-                    yield (END_TAG, self.getnsident())
-                    self.expect(">")
-                elif self.match("?"):
-                    yield (PI, self.getident())
-                    yield from self.lex_attrs_till()
-                    self.expect("?")
-                    self.expect(">")
-                else:
-                    tag = self.getnsident()
-                    yield (START_TAG, tag)
-                    yield from self.lex_attrs_till()
-                    if self.match("/"):
-                        yield (END_TAG, tag)
-                    self.expect(">")
-            else:
-                text = ""
-                while self.curch() != "<":
-                    text += self.getch()
-                if text:
-                    yield (TEXT, text)
-
-
-def gfind(gen, pred):
-    for i in gen:
-        if pred(i):
-            return i
-
-def text_of(gen, tag):
-    # Return text content of a leaf tag
-    def match_tag(t):
-        if t[0] != START_TAG:
-            return False
-        if isinstance(tag, ()):
-            return t[1] == tag
-        return t[1][1] == tag
-
-    gfind(gen, match_tag)
-    # Assumes no attributes
-    t, val = next(gen)
-    assert t == TEXT
-    return val
-
-def tokenize(file):
-    return XMLTokenizer(file).tokenize()
diff --git a/zipfile/metadata.txt b/zipfile/metadata.txt
deleted file mode 100644
index 976088c8a..000000000
--- a/zipfile/metadata.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-srctype = dummy
-type = module
-version = 0.0.0
diff --git a/zipfile/setup.py b/zipfile/setup.py
deleted file mode 100644
index 09ffcf8de..000000000
--- a/zipfile/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import sys
-# Remove current dir from sys.path, otherwise setuptools will peek up our
-# module instead of system.
-sys.path.pop(0)
-from setuptools import setup
-
-
-setup(name='micropython-zipfile',
-      version='0.0.0',
-      description='Dummy zipfile module for MicroPython',
-      long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.',
-      url='https://github.com/micropython/micropython/issues/405',
-      author='MicroPython Developers',
-      author_email='micro-python@googlegroups.com',
-      maintainer='MicroPython Developers',
-      maintainer_email='micro-python@googlegroups.com',
-      license='MIT',
-      py_modules=['zipfile'])
diff --git a/zipfile/zipfile.py b/zipfile/zipfile.py
deleted file mode 100644
index e69de29bb..000000000
