Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Python CI

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: python-ci-${{ github.ref }}
cancel-in-progress: true

jobs:
typing:
name: Static typing
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: pip
- run: python -m pip install ".[dev]"
- run: pyright

test:
name: Unit tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- run: python -m pip install .
- run: python -m unittest discover -s tests -v

coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: pip
- run: python -m pip install ".[dev]"
- run: coverage run -m unittest discover -s tests -v
- run: coverage report
- run: coverage xml
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml

package:
name: Build and verify distribution
needs: [typing, test, coverage]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: pip
- run: python -m pip install build
- run: python -m build
- run: python -m pip install --force-reinstall dist/*.whl
- name: Smoke-test the installed wheel outside the checkout
working-directory: /tmp
run: >-
python -c "from SmallPackage import SmallOS, SmallOSConfig, SmallTask, Unix;
assert SmallOS(config=SmallOSConfig()).setKernel(Unix())"
- uses: actions/upload-artifact@v4
with:
name: python-distributions
path: dist/
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
__pycache__/
*.py[cod]
.coverage
coverage.xml
htmlcov/
build/
dist/
*.egg-info/

150 changes: 148 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The project is currently experimental but usable. The runtime core supports:
- time-based sleeping
- readiness-based socket/I/O waiting
- generic TCP/TLS kernel hooks for higher-level protocols
- dependency-free thread and asyncio execution adapters for user libraries
- smallOS-native HTTP, Redis, MQTT, SSE, and WebSocket helper clients

## Why smallOS?
Expand Down Expand Up @@ -45,6 +46,8 @@ That makes it a good fit for:
buffered app/shell output routing and terminal-mode helpers
- [SmallPackage/clients](SmallPackage/clients):
protocol client package for cooperative network integrations
- [SmallPackage/adapters](SmallPackage/adapters):
dependency-free escape hatches for blocking and asyncio-owned user code
- [SmallPackage/clients/README.md](SmallPackage/clients/README.md):
detailed client-specific guide and API notes
- [SmallPackage/clients/SmallHTTP.py](SmallPackage/clients/SmallHTTP.py):
Expand Down Expand Up @@ -73,17 +76,51 @@ That makes it a good fit for:
Desktop development:

```bash
python3 -m venv .venv
python3.10 -m venv .venv
source .venv/bin/activate
pip install -e .
python -m pip install -e ".[dev]"
```

smallOS supports CPython 3.10 and newer. Python 3.6 through 3.9 are no
longer supported. MicroPython compatibility is maintained separately because
its language and standard-library support do not map directly to a CPython
release number; checker-only imports are kept off embedded runtime paths.

Run the test suite:

```bash
python3 -m unittest discover -s tests -v
```

Run the tests with the same branch-coverage gate used by CI:

```bash
coverage run -m unittest discover -s tests -v
coverage report
```

Run the static type checker:

```bash
pyright
```

Build the wheel and source distribution:

```bash
python -m build
```

The GitHub Actions pipeline runs four gates: Pyright, unit tests across Python
3.10–3.13, branch coverage with a 60% floor, and distribution verification.
Packaging runs only after the earlier gates pass, installs the built wheel,
and smoke-tests it outside the source checkout.

The package ships a `py.typed` marker. Type coverage is being tightened by
subsystem: configuration, awaitables, task lifecycle, scheduling, signals,
platform kernels, and core utilities form the current checked boundary, while
protocol clients, shells, and demos remain on the incremental typing backlog.

## Quick Start

Minimal desktop runtime:
Expand Down Expand Up @@ -140,6 +177,8 @@ task has been finalized. Current event fields include:
- `io_wait_mode`
- `join_target_id`
- `join_pending_ids`
- `adapter_name`
- `adapter_job_id`
- `traceback_text`

By default, `TaskCancelledError` does not trigger the handler. Pass
Expand Down Expand Up @@ -277,6 +316,12 @@ The new demos live in [demos](demos):
cooperative single-thread web app demo with HTTP routes, live browser UI, and shell-driven server shutdown
- [demos/mqtt_demo.py](demos/mqtt_demo.py):
MQTT example built on the native cooperative client
- [demos/adapters_demo.py](demos/adapters_demo.py):
thread and asyncio escape hatches running beside a regular SmallOS task
- [demos/adapters_sqlite_demo.py](demos/adapters_sqlite_demo.py):
a user-owned `sqlite3` connection kept on one thread-adapter worker
- [demos/adapters_asyncio_demo.py](demos/adapters_asyncio_demo.py):
a persistent asyncio queue and background task reused across adapter calls

All of the shared demo entry points now install a default runtime error handler
through [demos/common.py](demos/common.py). That means network failures,
Expand All @@ -302,6 +347,107 @@ This means arbitrary `asyncio` libraries are not drop-in compatible with the
runtime, but it also means scheduling policy and portability stay under your
control.

## Execution Adapters

Execution adapters let a SmallOS task yield while user-supplied code runs under
a different execution model. They use only the Python standard library and do
not install, import, configure, or wrap database drivers, ORMs, SDKs, or other
third-party packages.

Use `ThreadAdapter` for a synchronous blocking callable:

```python
from SmallPackage.adapters.threads import ThreadAdapter


def load_record(user_library, settings, record_id):
connection = user_library.connect(**settings)
try:
return connection.load(record_id)
finally:
connection.close()


with ThreadAdapter(max_workers=4, max_pending=64) as blocking:
async def load(task):
return await blocking.call(
load_record,
user_selected_library,
connection_settings,
42,
)

runtime.fork(SmallTask(2, load, name="load"))
runtime.start()
```

Use `AsyncioAdapter` for an async callable that must run on asyncio. The
adapter owns one persistent event loop in a dedicated thread, allowing
loop-affine clients to be reused when all their operations are routed through
the same adapter:

```python
from SmallPackage.adapters.asyncio_loop import AsyncioAdapter


async def fetch_record(user_library, settings, record_id):
async with user_library.Client(**settings) as client:
return await client.fetch(record_id)


with AsyncioAdapter(max_pending=64) as foreign_async:
async def load(task):
return await foreign_async.call(
fetch_record,
user_selected_async_library,
connection_settings,
42,
)

runtime.fork(SmallTask(2, load, name="load"))
runtime.start()
```

Important behavior:

- adapters bind to the first SmallOS runtime that uses them;
- adapter shutdown is explicit, so a context manager should wrap
`runtime.start()`;
- `max_pending` rejects excess work with `AdapterCapacityError` instead of
blocking the scheduler;
- cancelling a SmallTask can cancel queued thread work, but cannot forcibly
stop a running Python thread;
- asyncio cancellation is requested on the adapter loop, but a user library
may delay or suppress it;
- pass an async callable to `AsyncioAdapter.call()`, not a `Task` or `Future`
already owned by another loop;
- inspect `AsyncioAdapter.shutdown_error` after shutdown when application
diagnostics need to detect an unexpected loop stop or library teardown
failure; normal shutdown leaves it as `None`;
- use `ThreadAdapter(max_workers=1)` when user resources require a serialized,
thread-affine execution lane; create, use, and close those resources through
calls on that same adapter rather than creating them on the SmallOS thread.

### Standard-library examples

All adapter demos run without installing anything beyond SmallOS:

```bash
python3 demos/adapters_demo.py
python3 demos/adapters_sqlite_demo.py
python3 demos/adapters_asyncio_demo.py
```

- [demos/adapters_demo.py](demos/adapters_demo.py) runs both adapters beside an
ordinary cooperative SmallOS task.
- [demos/adapters_sqlite_demo.py](demos/adapters_sqlite_demo.py) creates, uses,
and closes an in-memory `sqlite3` connection through
`ThreadAdapter(max_workers=1)`. This is the ownership pattern to adapt for a
thread-affine PostgreSQL driver or ORM session supplied by the user.
- [demos/adapters_asyncio_demo.py](demos/adapters_asyncio_demo.py) creates an
`asyncio.Queue`, `Future` objects, and a background task, performs multiple
operations, and cleans them up on the same persistent adapter event loop.

## Running on MicroPython

For MicroPython targets, the intended flow is:
Expand Down
Loading
Loading