fromscratch-pynet 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fromscratch_pynet-0.2.0/.github/workflows/ci.yml +14 -0
- fromscratch_pynet-0.2.0/.github/workflows/docs.yml +16 -0
- fromscratch_pynet-0.2.0/.github/workflows/release.yml +58 -0
- fromscratch_pynet-0.2.0/.gitignore +14 -0
- fromscratch_pynet-0.2.0/.pre-commit-config.yaml +56 -0
- fromscratch_pynet-0.2.0/CLAUDE.md +53 -0
- fromscratch_pynet-0.2.0/CONTRIBUTING.md +199 -0
- fromscratch_pynet-0.2.0/LICENSE +21 -0
- fromscratch_pynet-0.2.0/PKG-INFO +250 -0
- fromscratch_pynet-0.2.0/README.md +217 -0
- fromscratch_pynet-0.2.0/conftest.py +21 -0
- fromscratch_pynet-0.2.0/docs/concepts/base64.md +63 -0
- fromscratch_pynet-0.2.0/docs/concepts/cancellation-and-timeouts.md +150 -0
- fromscratch_pynet-0.2.0/docs/concepts/dns.md +51 -0
- fromscratch_pynet-0.2.0/docs/concepts/event-loop.md +70 -0
- fromscratch_pynet-0.2.0/docs/concepts/framing.md +109 -0
- fromscratch_pynet-0.2.0/docs/concepts/ftp.md +52 -0
- fromscratch_pynet-0.2.0/docs/concepts/heartbeats-and-reconnect.md +398 -0
- fromscratch_pynet-0.2.0/docs/concepts/http-client.md +80 -0
- fromscratch_pynet-0.2.0/docs/concepts/json-client.md +44 -0
- fromscratch_pynet-0.2.0/docs/concepts/messaging.md +281 -0
- fromscratch_pynet-0.2.0/docs/concepts/our-loop-vs-asyncio.md +164 -0
- fromscratch_pynet-0.2.0/docs/concepts/ping.md +56 -0
- fromscratch_pynet-0.2.0/docs/concepts/self-pipe-trick.md +65 -0
- fromscratch_pynet-0.2.0/docs/concepts/smtp.md +58 -0
- fromscratch_pynet-0.2.0/docs/concepts/streams.md +160 -0
- fromscratch_pynet-0.2.0/docs/concepts/subprocesses.md +169 -0
- fromscratch_pynet-0.2.0/docs/concepts/sync-primitives.md +86 -0
- fromscratch_pynet-0.2.0/docs/concepts/tasks-and-await.md +154 -0
- fromscratch_pynet-0.2.0/docs/concepts/tcp.md +52 -0
- fromscratch_pynet-0.2.0/docs/concepts/threads.md +73 -0
- fromscratch_pynet-0.2.0/docs/concepts/timers.md +104 -0
- fromscratch_pynet-0.2.0/docs/concepts/tls.md +73 -0
- fromscratch_pynet-0.2.0/docs/concepts/udp.md +46 -0
- fromscratch_pynet-0.2.0/docs/concepts/urls.md +60 -0
- fromscratch_pynet-0.2.0/docs/concepts/websocket-real.md +357 -0
- fromscratch_pynet-0.2.0/docs/concepts/websocket.md +66 -0
- fromscratch_pynet-0.2.0/docs/index.md +114 -0
- fromscratch_pynet-0.2.0/docs/property-testing.md +349 -0
- fromscratch_pynet-0.2.0/docs/superpowers/plans/2026-10-16-pynet-event-loop.md +656 -0
- fromscratch_pynet-0.2.0/docs/superpowers/specs/2026-10-16-pynet-event-loop-design.md +365 -0
- fromscratch_pynet-0.2.0/examples/chat_room.py +107 -0
- fromscratch_pynet-0.2.0/examples/echo_server.py +65 -0
- fromscratch_pynet-0.2.0/examples/heartbeat_demo.py +89 -0
- fromscratch_pynet-0.2.0/examples/job_runner.py +95 -0
- fromscratch_pynet-0.2.0/examples/ws_echo.py +44 -0
- fromscratch_pynet-0.2.0/mkdocs.yml +101 -0
- fromscratch_pynet-0.2.0/overrides/main.html +7 -0
- fromscratch_pynet-0.2.0/pyproject.toml +162 -0
- fromscratch_pynet-0.2.0/src/pynet/__init__.py +1 -0
- fromscratch_pynet-0.2.0/src/pynet/_digits.py +16 -0
- fromscratch_pynet-0.2.0/src/pynet/_version.py +24 -0
- fromscratch_pynet-0.2.0/src/pynet/base64.py +138 -0
- fromscratch_pynet-0.2.0/src/pynet/cli.py +28 -0
- fromscratch_pynet-0.2.0/src/pynet/dns.py +79 -0
- fromscratch_pynet-0.2.0/src/pynet/errors.py +88 -0
- fromscratch_pynet-0.2.0/src/pynet/framing.py +171 -0
- fromscratch_pynet-0.2.0/src/pynet/ftp.py +54 -0
- fromscratch_pynet-0.2.0/src/pynet/http.py +271 -0
- fromscratch_pynet-0.2.0/src/pynet/json_client.py +82 -0
- fromscratch_pynet-0.2.0/src/pynet/loop/__init__.py +46 -0
- fromscratch_pynet-0.2.0/src/pynet/loop/children.py +351 -0
- fromscratch_pynet-0.2.0/src/pynet/loop/control.py +237 -0
- fromscratch_pynet-0.2.0/src/pynet/loop/futures.py +187 -0
- fromscratch_pynet-0.2.0/src/pynet/loop/reactor.py +502 -0
- fromscratch_pynet-0.2.0/src/pynet/loop/sync.py +295 -0
- fromscratch_pynet-0.2.0/src/pynet/loop/tasks.py +296 -0
- fromscratch_pynet-0.2.0/src/pynet/loop/testing.py +37 -0
- fromscratch_pynet-0.2.0/src/pynet/loop/threads.py +164 -0
- fromscratch_pynet-0.2.0/src/pynet/messaging/__init__.py +92 -0
- fromscratch_pynet-0.2.0/src/pynet/messaging/codec.py +244 -0
- fromscratch_pynet-0.2.0/src/pynet/messaging/peer.py +659 -0
- fromscratch_pynet-0.2.0/src/pynet/messaging/reconnect.py +338 -0
- fromscratch_pynet-0.2.0/src/pynet/ping.py +64 -0
- fromscratch_pynet-0.2.0/src/pynet/process.py +236 -0
- fromscratch_pynet-0.2.0/src/pynet/py.typed +0 -0
- fromscratch_pynet-0.2.0/src/pynet/smtp.py +76 -0
- fromscratch_pynet-0.2.0/src/pynet/streams.py +861 -0
- fromscratch_pynet-0.2.0/src/pynet/tcp.py +129 -0
- fromscratch_pynet-0.2.0/src/pynet/tls.py +81 -0
- fromscratch_pynet-0.2.0/src/pynet/udp.py +53 -0
- fromscratch_pynet-0.2.0/src/pynet/url.py +117 -0
- fromscratch_pynet-0.2.0/src/pynet/websocket.py +90 -0
- fromscratch_pynet-0.2.0/src/pynet/ws/__init__.py +81 -0
- fromscratch_pynet-0.2.0/src/pynet/ws/connection.py +393 -0
- fromscratch_pynet-0.2.0/src/pynet/ws/endpoints.py +200 -0
- fromscratch_pynet-0.2.0/src/pynet/ws/frames.py +257 -0
- fromscratch_pynet-0.2.0/src/pynet/ws/handshake.py +206 -0
- fromscratch_pynet-0.2.0/tests/__init__.py +1 -0
- fromscratch_pynet-0.2.0/tests/loop/__init__.py +1 -0
- fromscratch_pynet-0.2.0/tests/loop/test_control.py +425 -0
- fromscratch_pynet-0.2.0/tests/loop/test_io.py +242 -0
- fromscratch_pynet-0.2.0/tests/loop/test_reactor.py +399 -0
- fromscratch_pynet-0.2.0/tests/loop/test_sync.py +402 -0
- fromscratch_pynet-0.2.0/tests/loop/test_tasks.py +782 -0
- fromscratch_pynet-0.2.0/tests/loop/test_threads.py +302 -0
- fromscratch_pynet-0.2.0/tests/pretend_pipe.py +164 -0
- fromscratch_pynet-0.2.0/tests/strategies.py +117 -0
- fromscratch_pynet-0.2.0/tests/test_base64.py +126 -0
- fromscratch_pynet-0.2.0/tests/test_digits.py +29 -0
- fromscratch_pynet-0.2.0/tests/test_dns.py +75 -0
- fromscratch_pynet-0.2.0/tests/test_examples.py +46 -0
- fromscratch_pynet-0.2.0/tests/test_framing.py +271 -0
- fromscratch_pynet-0.2.0/tests/test_ftp.py +89 -0
- fromscratch_pynet-0.2.0/tests/test_heartbeat.py +452 -0
- fromscratch_pynet-0.2.0/tests/test_http.py +266 -0
- fromscratch_pynet-0.2.0/tests/test_json_client.py +53 -0
- fromscratch_pynet-0.2.0/tests/test_messaging_codec.py +311 -0
- fromscratch_pynet-0.2.0/tests/test_model.py +141 -0
- fromscratch_pynet-0.2.0/tests/test_peer.py +893 -0
- fromscratch_pynet-0.2.0/tests/test_ping.py +55 -0
- fromscratch_pynet-0.2.0/tests/test_process.py +589 -0
- fromscratch_pynet-0.2.0/tests/test_properties.py +360 -0
- fromscratch_pynet-0.2.0/tests/test_reconnect.py +584 -0
- fromscratch_pynet-0.2.0/tests/test_smtp.py +91 -0
- fromscratch_pynet-0.2.0/tests/test_streams.py +758 -0
- fromscratch_pynet-0.2.0/tests/test_tcp.py +120 -0
- fromscratch_pynet-0.2.0/tests/test_tls.py +103 -0
- fromscratch_pynet-0.2.0/tests/test_udp.py +59 -0
- fromscratch_pynet-0.2.0/tests/test_url.py +90 -0
- fromscratch_pynet-0.2.0/tests/test_websocket.py +98 -0
- fromscratch_pynet-0.2.0/tests/test_ws_connection.py +707 -0
- fromscratch_pynet-0.2.0/tests/test_ws_frames.py +221 -0
- fromscratch_pynet-0.2.0/tests/test_ws_handshake.py +189 -0
- fromscratch_pynet-0.2.0/uv.lock +934 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Deploy Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch: {}
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
docs:
|
|
10
|
+
uses: robcox2910/robcox2910.github.io/.github/workflows/docs.yml@v1
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pages: write
|
|
14
|
+
id-token: write
|
|
15
|
+
with:
|
|
16
|
+
coverage-report: false
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v7
|
|
22
|
+
with:
|
|
23
|
+
version: "latest"
|
|
24
|
+
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
run: uv python install 3.14
|
|
27
|
+
|
|
28
|
+
- name: Build distributions
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Upload build artifacts
|
|
32
|
+
uses: actions/upload-artifact@v7
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
- name: Upload to GitHub Release
|
|
38
|
+
uses: softprops/action-gh-release@v3
|
|
39
|
+
with:
|
|
40
|
+
files: dist/*
|
|
41
|
+
|
|
42
|
+
publish:
|
|
43
|
+
needs: build
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
environment:
|
|
46
|
+
name: pypi
|
|
47
|
+
url: https://pypi.org/project/fromscratch-pynet/
|
|
48
|
+
permissions:
|
|
49
|
+
id-token: write
|
|
50
|
+
steps:
|
|
51
|
+
- name: Download build artifacts
|
|
52
|
+
uses: actions/download-artifact@v8
|
|
53
|
+
with:
|
|
54
|
+
name: dist
|
|
55
|
+
path: dist/
|
|
56
|
+
|
|
57
|
+
- name: Publish to PyPI
|
|
58
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
default_language_version:
|
|
2
|
+
python: python3.14
|
|
3
|
+
|
|
4
|
+
repos:
|
|
5
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
6
|
+
rev: v5.0.0
|
|
7
|
+
hooks:
|
|
8
|
+
# Pages paste real pytest output, trailing spaces included.
|
|
9
|
+
- id: trailing-whitespace
|
|
10
|
+
exclude: ^docs/property-testing\.md$
|
|
11
|
+
- id: end-of-file-fixer
|
|
12
|
+
exclude: ^docs/property-testing\.md$
|
|
13
|
+
- id: check-yaml
|
|
14
|
+
args: ['--unsafe']
|
|
15
|
+
- id: check-added-large-files
|
|
16
|
+
- id: check-json
|
|
17
|
+
- id: check-toml
|
|
18
|
+
- id: check-merge-conflict
|
|
19
|
+
- id: debug-statements
|
|
20
|
+
|
|
21
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
22
|
+
rev: v0.15.4
|
|
23
|
+
hooks:
|
|
24
|
+
- id: ruff
|
|
25
|
+
args: [--fix, --exit-non-zero-on-fix]
|
|
26
|
+
- id: ruff-format
|
|
27
|
+
|
|
28
|
+
- repo: https://github.com/RobertCraigie/pyright-python
|
|
29
|
+
rev: v1.1.408
|
|
30
|
+
hooks:
|
|
31
|
+
- id: pyright
|
|
32
|
+
|
|
33
|
+
- repo: local
|
|
34
|
+
hooks:
|
|
35
|
+
- id: pip-audit
|
|
36
|
+
name: pip-audit
|
|
37
|
+
language: system
|
|
38
|
+
pass_filenames: false
|
|
39
|
+
files: ^(pyproject\.toml|uv\.lock)$
|
|
40
|
+
entry: >-
|
|
41
|
+
bash -c 'f=$(mktemp) &&
|
|
42
|
+
uv export --frozen --no-emit-project --all-extras --no-hashes -o "$f" >/dev/null &&
|
|
43
|
+
uvx pip-audit==2.10.0 -r "$f" --disable-pip --no-deps;
|
|
44
|
+
status=$?; rm -f "$f"; exit $status'
|
|
45
|
+
|
|
46
|
+
- repo: https://github.com/rhysd/actionlint
|
|
47
|
+
rev: v1.7.7
|
|
48
|
+
hooks:
|
|
49
|
+
- id: actionlint
|
|
50
|
+
|
|
51
|
+
- repo: https://github.com/commitizen-tools/commitizen
|
|
52
|
+
rev: v4.13.8
|
|
53
|
+
hooks:
|
|
54
|
+
- id: commitizen
|
|
55
|
+
stages: [commit-msg]
|
|
56
|
+
additional_dependencies: [typing_extensions]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Claude Code Instructions
|
|
2
|
+
|
|
3
|
+
<!-- standard:start -->
|
|
4
|
+
## Code Style
|
|
5
|
+
|
|
6
|
+
- All public functions, methods, classes, and modules **must** have a docstring
|
|
7
|
+
- Use imperative mood: "Return the result." not "Returns the result."
|
|
8
|
+
- One-line docstrings for simple functions; multi-line with summary + details for complex ones
|
|
9
|
+
- Follow the Google docstring convention (D211, D212 selected via ruff)
|
|
10
|
+
|
|
11
|
+
## Linting
|
|
12
|
+
|
|
13
|
+
- Ruff enforces a comprehensive rule set -- run `uv run ruff check .` before committing
|
|
14
|
+
- Pyright runs in strict mode -- run `uv run pyright src tests` before committing
|
|
15
|
+
- All violations must be fixed, not ignored. Exceptions are the repo-owned `ignore` and `per-file-ignores` lists in `pyproject.toml`, which belong to this repo; the series sync never edits them
|
|
16
|
+
|
|
17
|
+
## Testing
|
|
18
|
+
|
|
19
|
+
- TDD workflow: Red-Green-Refactor
|
|
20
|
+
- Coverage is enforced by pytest-cov; the floor in `pyproject.toml` is repo-owned
|
|
21
|
+
- Test classes and methods also require docstrings
|
|
22
|
+
- Use named constants instead of magic values (PLR2004)
|
|
23
|
+
- Run `uv run pytest` before committing
|
|
24
|
+
- Property-based tests (Hypothesis) live in `tests/test_properties.py` and `tests/test_model.py`; the shared profiles are in the root `conftest.py` (owned by `series sync`). Run `uv run pytest --hypothesis-profile=thorough` before a release.
|
|
25
|
+
|
|
26
|
+
## Commits
|
|
27
|
+
|
|
28
|
+
- Use conventional commits: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `ci:`, `chore:`
|
|
29
|
+
- All changes go via PR from a feature branch (main is protected); merge when CI is green
|
|
30
|
+
|
|
31
|
+
## Dependencies
|
|
32
|
+
|
|
33
|
+
- Package manager: uv
|
|
34
|
+
- Run `uv sync --all-extras` after modifying dependencies
|
|
35
|
+
- `uv.lock` is committed; CI and pre-commit audit it with pip-audit through `uvx`, so pip-audit is not a project dependency
|
|
36
|
+
- Sibling projects, where used, are pinned release tags in `[tool.uv.sources]` (git + tag); never use `path` sources
|
|
37
|
+
|
|
38
|
+
## Releasing
|
|
39
|
+
|
|
40
|
+
- A release is a `vX.Y.Z` tag on main after the PR is merged
|
|
41
|
+
- Docs-only and tooling-only changes do not get a release tag
|
|
42
|
+
- Docs are built with `mkdocs build --strict` and deployed to GitHub Pages by the docs workflow
|
|
43
|
+
<!-- standard:end -->
|
|
44
|
+
|
|
45
|
+
## Project notes
|
|
46
|
+
|
|
47
|
+
- Use imperative mood: "Return the response." not "Returns the response."
|
|
48
|
+
- Pyright in strict mode -- run `uv run pyright src tests` before committing
|
|
49
|
+
- All violations must be fixed, not ignored (except D203/D213 incompatible pair)
|
|
50
|
+
- Minimum 80% coverage enforced by pytest-cov
|
|
51
|
+
- Use conventional commits (enforced by commitizen): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `ci:`, `chore:`
|
|
52
|
+
- All changes go via PR from a feature branch (main is protected)
|
|
53
|
+
- Add production deps to `[project.dependencies]`
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Contributing to PyNet
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing! This guide covers everything you need
|
|
4
|
+
to get started.
|
|
5
|
+
|
|
6
|
+
## Prerequisites
|
|
7
|
+
|
|
8
|
+
| Tool | Version | Install |
|
|
9
|
+
|------|---------|---------|
|
|
10
|
+
| Python | 3.14+ | [python.org](https://www.python.org/downloads/) |
|
|
11
|
+
| uv | latest | `curl -LsSf https://astral.sh/uv/install.sh \| sh` |
|
|
12
|
+
| git | 2.x+ | [git-scm.com](https://git-scm.com/) |
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Clone the repo
|
|
18
|
+
git clone https://github.com/robcox2910/pynet.git
|
|
19
|
+
cd pynet
|
|
20
|
+
|
|
21
|
+
# Install all dependencies (including dev tools)
|
|
22
|
+
uv sync --all-extras
|
|
23
|
+
|
|
24
|
+
# Install pre-commit hooks (linting + commit message validation)
|
|
25
|
+
uv run pre-commit install
|
|
26
|
+
uv run pre-commit install --hook-type commit-msg
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Verify everything works:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv run pytest --cov # tests pass, >= 80% coverage
|
|
33
|
+
uv run ruff check src/ tests/ # no lint errors
|
|
34
|
+
uv run pyright src tests # no type errors
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## TDD Workflow
|
|
38
|
+
|
|
39
|
+
We follow **Red-Green-Refactor** for every code change:
|
|
40
|
+
|
|
41
|
+
1. **Red** -- Write a failing test that describes the behaviour you want.
|
|
42
|
+
2. **Green** -- Write the simplest code that makes the test pass.
|
|
43
|
+
3. **Refactor** -- Clean up while keeping all tests green.
|
|
44
|
+
|
|
45
|
+
Never skip the red step. If you can't write a failing test first, take a moment
|
|
46
|
+
to think about what the code should actually do.
|
|
47
|
+
|
|
48
|
+
## Branch Naming
|
|
49
|
+
|
|
50
|
+
Create a branch from `main` using one of these prefixes:
|
|
51
|
+
|
|
52
|
+
| Prefix | When to use |
|
|
53
|
+
|--------|-------------|
|
|
54
|
+
| `feat/` | New networking feature or protocol support |
|
|
55
|
+
| `fix/` | Bug fix |
|
|
56
|
+
| `refactor/` | Restructuring without behaviour change |
|
|
57
|
+
| `test/` | Test-only changes |
|
|
58
|
+
| `docs/` | Documentation updates |
|
|
59
|
+
| `ci/` | CI/CD workflow changes |
|
|
60
|
+
| `chore/` | Dependency bumps, config tweaks |
|
|
61
|
+
|
|
62
|
+
Example: `feat/websocket-client`, `fix/dns-timeout-handling`
|
|
63
|
+
|
|
64
|
+
## Conventional Commits
|
|
65
|
+
|
|
66
|
+
Every commit message must follow [Conventional Commits](https://www.conventionalcommits.org/):
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
<type>: <short description in imperative mood>
|
|
70
|
+
|
|
71
|
+
Optional longer explanation.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Types match the branch prefixes: `feat:`, `fix:`, `refactor:`, `test:`,
|
|
75
|
+
`docs:`, `ci:`, `chore:`.
|
|
76
|
+
|
|
77
|
+
Good examples:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
feat: add HTTP POST support
|
|
81
|
+
fix: handle DNS timeout for unreachable hosts
|
|
82
|
+
docs: add URL parsing concept page
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Bad examples:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
updated stuff # no type, vague description
|
|
89
|
+
feat: adds routing # not imperative mood ("adds" -> "add")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Code Style
|
|
93
|
+
|
|
94
|
+
### Modern Python
|
|
95
|
+
|
|
96
|
+
Use Python 3.14 idioms everywhere:
|
|
97
|
+
|
|
98
|
+
- `X | Y` for union types (not `Union[X, Y]` or `Optional[X]`)
|
|
99
|
+
- `StrEnum` for string enumerations
|
|
100
|
+
- `match/case` instead of if/elif chains where appropriate
|
|
101
|
+
- Direct imports: `from enum import StrEnum` not `import enum`
|
|
102
|
+
|
|
103
|
+
### Docstrings
|
|
104
|
+
|
|
105
|
+
Every public function, method, class, and module needs a docstring:
|
|
106
|
+
|
|
107
|
+
- Use **imperative mood**: "Return the record." not "Returns the record."
|
|
108
|
+
- One-liner for simple functions, multi-line (Google style) for complex ones.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
def resolve(hostname: str) -> str:
|
|
112
|
+
"""Look up the IP address for a hostname."""
|
|
113
|
+
|
|
114
|
+
def parse_url(
|
|
115
|
+
raw: str,
|
|
116
|
+
default_scheme: str = "http",
|
|
117
|
+
) -> URL:
|
|
118
|
+
"""Parse a URL string into its components.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
raw: The URL string (e.g., "https://example.com/path").
|
|
122
|
+
default_scheme: Scheme to use if none is specified.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
A parsed URL object with scheme, host, port, path, query, and fragment.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: If the URL is empty.
|
|
129
|
+
"""
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Named Constants
|
|
133
|
+
|
|
134
|
+
Use named constants instead of magic numbers in tests and source code. Ruff
|
|
135
|
+
rule PLR2004 enforces this.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
# Good
|
|
139
|
+
HTTP_DEFAULT_PORT = 80
|
|
140
|
+
assert result.port == HTTP_DEFAULT_PORT
|
|
141
|
+
|
|
142
|
+
# Bad
|
|
143
|
+
assert result.port == 80
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Linting and Type Checking
|
|
147
|
+
|
|
148
|
+
Run these before every commit (pre-commit hooks enforce them automatically):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
uv run ruff check src/ tests/ # lint
|
|
152
|
+
uv run ruff format src/ tests/ # format (or --check to verify)
|
|
153
|
+
uv run pyright src tests # type check (strict mode)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Fix all violations. Inline `# noqa:` is acceptable only when tools genuinely
|
|
157
|
+
conflict (e.g., ARG002 vs pyright protocol parameter names).
|
|
158
|
+
|
|
159
|
+
## Testing
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
uv run pytest # run all tests
|
|
163
|
+
uv run pytest --cov # with coverage report
|
|
164
|
+
uv run pytest tests/test_x.py # run a specific file
|
|
165
|
+
uv run pytest -k "test_name" # run tests matching a pattern
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- Minimum **80% coverage** is enforced.
|
|
169
|
+
- Test classes and methods need docstrings too.
|
|
170
|
+
- Use `pytest.raises` as a context manager for exception testing.
|
|
171
|
+
|
|
172
|
+
## Documentation Style
|
|
173
|
+
|
|
174
|
+
Our docs target a **12-year-old with an interest in computing**. Keep language
|
|
175
|
+
simple, use analogies, and avoid jargon without explanation.
|
|
176
|
+
|
|
177
|
+
Good:
|
|
178
|
+
|
|
179
|
+
> DNS is like the phone book of the internet: you look up a name and get a
|
|
180
|
+
> number you can actually call.
|
|
181
|
+
|
|
182
|
+
Bad:
|
|
183
|
+
|
|
184
|
+
> DNS is a hierarchical decentralised naming system that maps domain names to
|
|
185
|
+
> IP addresses using a distributed database architecture.
|
|
186
|
+
|
|
187
|
+
## Pull Request Process
|
|
188
|
+
|
|
189
|
+
1. Create a feature branch from `main`.
|
|
190
|
+
2. Make your changes following TDD.
|
|
191
|
+
3. Ensure all checks pass: `uv run ruff check .`, `uv run pyright src tests`,
|
|
192
|
+
`uv run pytest --cov`.
|
|
193
|
+
4. Push your branch and open a PR against `main`.
|
|
194
|
+
5. PRs are squash-merged to keep history clean.
|
|
195
|
+
|
|
196
|
+
## Questions?
|
|
197
|
+
|
|
198
|
+
Open an issue on [GitHub](https://github.com/robcox2910/pynet/issues) --
|
|
199
|
+
we're happy to help!
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rob Cox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fromscratch-pynet
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: An educational networking library built from scratch in Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/robcox2910/pynet
|
|
6
|
+
Project-URL: Repository, https://github.com/robcox2910/pynet
|
|
7
|
+
Project-URL: Issues, https://github.com/robcox2910/pynet/issues
|
|
8
|
+
Project-URL: Documentation, https://robcox2910.github.io/pynet/
|
|
9
|
+
Author: Rob Cox
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: dns,education,http-client,networking,tcp
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Education
|
|
19
|
+
Classifier: Topic :: Internet
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.14
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: commitizen>=4.4.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: hypothesis>=6.170; extra == 'dev'
|
|
25
|
+
Requires-Dist: pip-audit>=2.10.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pre-commit>=4.0.1; extra == 'dev'
|
|
27
|
+
Requires-Dist: pyright>=1.1.400; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-timeout>=2.3.1; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.3.4; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.9.3; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# PyNet
|
|
35
|
+
|
|
36
|
+
**An educational networking library built from scratch in Python.**
|
|
37
|
+
|
|
38
|
+
## Telephones for Computers
|
|
39
|
+
|
|
40
|
+
Imagine you want to talk to your friend who lives across town. You pick up the
|
|
41
|
+
phone, dial their number, and start chatting. The internet works the same way!
|
|
42
|
+
|
|
43
|
+
When your computer visits a website, it:
|
|
44
|
+
|
|
45
|
+
1. **Reads the address** -- like reading a phone number off a sticky note
|
|
46
|
+
(URL parsing)
|
|
47
|
+
2. **Looks up the number** -- like finding someone in a phone book
|
|
48
|
+
(DNS lookup)
|
|
49
|
+
3. **Dials and talks** -- like making the phone call and having a conversation
|
|
50
|
+
(HTTP request and response)
|
|
51
|
+
|
|
52
|
+
PyNet teaches you how all of this works by building each piece from scratch.
|
|
53
|
+
|
|
54
|
+
## Examples
|
|
55
|
+
|
|
56
|
+
### URL Parsing -- Reading the Address
|
|
57
|
+
|
|
58
|
+
A URL is like a phone number with extra info: who to call, what department,
|
|
59
|
+
and what to ask about.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from pynet.url import parse_url
|
|
63
|
+
|
|
64
|
+
url = parse_url("https://example.com:8080/api/users?page=2#results")
|
|
65
|
+
|
|
66
|
+
print(url.scheme) # "https" -- how to connect
|
|
67
|
+
print(url.host) # "example.com" -- who to call
|
|
68
|
+
print(url.port) # 8080 -- which door to knock on
|
|
69
|
+
print(url.path) # "/api/users" -- what to ask for
|
|
70
|
+
print(url.query) # {"page": "2"} -- extra details
|
|
71
|
+
print(url.fragment) # "results" -- where to scroll on the page
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### DNS Lookup -- The Phone Book
|
|
75
|
+
|
|
76
|
+
DNS translates names like `example.com` into numbers like `93.184.216.34`
|
|
77
|
+
that computers actually use to find each other.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from pynet.dns import resolve, is_valid_ipv4
|
|
81
|
+
|
|
82
|
+
ip = resolve("localhost")
|
|
83
|
+
print(ip) # "127.0.0.1" -- your own computer!
|
|
84
|
+
|
|
85
|
+
print(is_valid_ipv4("192.168.1.1")) # True
|
|
86
|
+
print(is_valid_ipv4("999.0.0.1")) # False
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### HTTP GET -- Making the Call
|
|
90
|
+
|
|
91
|
+
An HTTP request is like picking up the phone and asking a question. The server
|
|
92
|
+
picks up and gives you an answer.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from pynet.http import get
|
|
96
|
+
|
|
97
|
+
response = get("http://example.com/")
|
|
98
|
+
print(response.status) # 200 (OK -- the server understood!)
|
|
99
|
+
print(response.body[:50]) # The first 50 characters of the page
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### JSON API -- Structured Conversations
|
|
103
|
+
|
|
104
|
+
When programs talk to each other, they use JSON -- a structured format both
|
|
105
|
+
sides understand, like filling out a form instead of writing a free-form letter.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from pynet.json_client import json_get
|
|
109
|
+
|
|
110
|
+
data = json_get("http://api.example.com/users/1")
|
|
111
|
+
print(data["name"]) # The user's name from the API
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Real Networking -- A Phone Line That Rings
|
|
115
|
+
|
|
116
|
+
PyNet has its own event loop, built from scratch, and real sockets that run
|
|
117
|
+
on it. This echo server and client talk over a real TCP connection on your
|
|
118
|
+
own computer:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from pynet.loop import run
|
|
122
|
+
from pynet.streams import open_connection, start_server
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def echo(reader, writer):
|
|
126
|
+
data = await reader.read(1024)
|
|
127
|
+
writer.write(data)
|
|
128
|
+
await writer.drain()
|
|
129
|
+
writer.close()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def main():
|
|
133
|
+
async with await start_server(echo, "127.0.0.1", 0) as server:
|
|
134
|
+
reader, writer = await open_connection("127.0.0.1", server.port)
|
|
135
|
+
writer.write(b"hello, phone line!")
|
|
136
|
+
await writer.drain()
|
|
137
|
+
print(await reader.read(1024))
|
|
138
|
+
writer.close()
|
|
139
|
+
await writer.wait_closed()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
run(main()) # b'hello, phone line!'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Features
|
|
146
|
+
|
|
147
|
+
- **URL Parsing** -- Break apart web addresses into their components
|
|
148
|
+
- **DNS Resolution** -- Translate hostnames into IP addresses
|
|
149
|
+
- **HTTP Client** -- Send GET and POST requests over raw TCP sockets
|
|
150
|
+
- **JSON Client** -- Talk to web APIs with structured data
|
|
151
|
+
- **Custom Errors** -- Clear error types for DNS, connection, and HTTP failures
|
|
152
|
+
- **TCP** -- Reliable, ordered byte-stream delivery
|
|
153
|
+
- **UDP** -- Fast, connectionless datagrams
|
|
154
|
+
- **WebSocket** -- Full-duplex communication over a single connection
|
|
155
|
+
- **SMTP** -- Send emails programmatically
|
|
156
|
+
- **FTP** -- Transfer files between machines
|
|
157
|
+
- **Ping/ICMP** -- Check if a host is reachable
|
|
158
|
+
- **Base64** -- Encode and decode binary data as text
|
|
159
|
+
- **TLS** -- Encrypted connections with certificates
|
|
160
|
+
- **Event Loop** -- A from-scratch `select`-based loop with timers, tasks,
|
|
161
|
+
`await`, cancellation, timeouts, and sync primitives (Event, Queue, Lock,
|
|
162
|
+
Semaphore)
|
|
163
|
+
- **Real TCP Streams** -- Non-blocking `start_server` and `open_connection`
|
|
164
|
+
with backpressure
|
|
165
|
+
- **Subprocesses** -- Run many commands at once and kill whole process groups
|
|
166
|
+
- **Framing and Messaging** -- Length-prefixed frames and request/reply/push
|
|
167
|
+
messages with binary attachments
|
|
168
|
+
- **Heartbeats and Reconnect** -- Ping/pong liveness checks and redialling
|
|
169
|
+
with exponential backoff and jitter
|
|
170
|
+
- **Real WebSocket** -- RFC 6455 handshake, frames, and live connections
|
|
171
|
+
- **CLI Tool** -- Interactive demo of networking concepts
|
|
172
|
+
- **100% Typed** -- Full type annotations with strict Pyright checking
|
|
173
|
+
|
|
174
|
+
## Quick Start
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Install with uv
|
|
178
|
+
uv add pynet
|
|
179
|
+
|
|
180
|
+
# Or install from source
|
|
181
|
+
git clone https://github.com/robcox2910/pynet.git
|
|
182
|
+
cd pynet
|
|
183
|
+
uv sync --all-extras
|
|
184
|
+
|
|
185
|
+
# Run the CLI demo
|
|
186
|
+
uv run pynet
|
|
187
|
+
|
|
188
|
+
# Run the tests
|
|
189
|
+
uv run pytest
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Examples to Run
|
|
193
|
+
|
|
194
|
+
Each file in `examples/` runs with `uv run python examples/<name>.py`:
|
|
195
|
+
|
|
196
|
+
- `echo_server.py` -- a parrot on a real TCP line (try it with `nc 127.0.0.1 8888`)
|
|
197
|
+
- `job_runner.py` -- start three commands at once and watch each finish
|
|
198
|
+
- `chat_room.py` -- a group channel built on messaging requests and pushes
|
|
199
|
+
- `heartbeat_demo.py` -- watch pings go out and a frozen server get noticed
|
|
200
|
+
- `ws_echo.py` -- a WebSocket echo server you can connect to from a browser
|
|
201
|
+
|
|
202
|
+
<!-- docs:start -->
|
|
203
|
+
## Documentation
|
|
204
|
+
|
|
205
|
+
Full docs (with kid-friendly explanations of every idea): https://robcox2910.github.io/pynet/
|
|
206
|
+
<!-- docs:end -->
|
|
207
|
+
|
|
208
|
+
## Releasing
|
|
209
|
+
|
|
210
|
+
Other projects in the series ([PyStack](https://github.com/robcox2910/pystack)) use a pinned release of this one,
|
|
211
|
+
not whatever is on `main`. To publish a change for them:
|
|
212
|
+
|
|
213
|
+
1. Merge the change to `main`.
|
|
214
|
+
2. Tag it: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`.
|
|
215
|
+
A change to the public API bumps the minor version (`v0.1.0` → `v0.2.0`);
|
|
216
|
+
anything else bumps the patch (`v0.1.0` → `v0.1.1`).
|
|
217
|
+
3. In each project that uses it, edit the tag in `[tool.uv.sources]`, run
|
|
218
|
+
`uv lock --upgrade-package <name>`, and open a pull request so CI checks
|
|
219
|
+
the pair together.
|
|
220
|
+
|
|
221
|
+
<!-- series:start -->
|
|
222
|
+
## Related Projects
|
|
223
|
+
|
|
224
|
+
This project is one of a series that builds real systems "from scratch in Python" to teach how they work. See the [series home](https://robcox2910.github.io/) for the full story.
|
|
225
|
+
|
|
226
|
+
| Project | What it teaches | Repository | Docs |
|
|
227
|
+
| --- | --- | --- | --- |
|
|
228
|
+
| PyOS | Operating systems | [py-os](https://github.com/robcox2910/py-os) | [Docs](https://robcox2910.github.io/py-os/) |
|
|
229
|
+
| Pebble | Compilers and programming languages | [pebble-lang](https://github.com/robcox2910/pebble-lang) | [Docs](https://robcox2910.github.io/pebble-lang/) |
|
|
230
|
+
| PyDB | Relational databases | [pydb](https://github.com/robcox2910/pydb) | [Docs](https://robcox2910.github.io/pydb/) |
|
|
231
|
+
| PyKV | Key-value stores | [pykv](https://github.com/robcox2910/pykv) | [Docs](https://robcox2910.github.io/pykv/) |
|
|
232
|
+
| PyDocDB | Document databases | [pydoc-db](https://github.com/robcox2910/pydoc-db) | [Docs](https://robcox2910.github.io/pydoc-db/) |
|
|
233
|
+
| PyGraphDB | Graph databases | [pygraphdb](https://github.com/robcox2910/pygraphdb) | [Docs](https://robcox2910.github.io/pygraphdb/) |
|
|
234
|
+
| PyTSDB | Time-series databases | [pytsdb](https://github.com/robcox2910/pytsdb) | [Docs](https://robcox2910.github.io/pytsdb/) |
|
|
235
|
+
| PyVecDB | Vector databases | [pyvecdb](https://github.com/robcox2910/pyvecdb) | [Docs](https://robcox2910.github.io/pyvecdb/) |
|
|
236
|
+
| PyColDB | Column-family databases | [pycoldb](https://github.com/robcox2910/pycoldb) | [Docs](https://robcox2910.github.io/pycoldb/) |
|
|
237
|
+
| PyCrypt | Cryptography | [pycrypt](https://github.com/robcox2910/pycrypt) | [Docs](https://robcox2910.github.io/pycrypt/) |
|
|
238
|
+
| **PyNet** | **Computer networking** | [pynet](https://github.com/robcox2910/pynet) | [Docs](https://robcox2910.github.io/pynet/) |
|
|
239
|
+
| PyWeb | HTTP web servers | [pyweb](https://github.com/robcox2910/pyweb) | [Docs](https://robcox2910.github.io/pyweb/) |
|
|
240
|
+
| PyGit | Version control | [pygit](https://github.com/robcox2910/pygit) | [Docs](https://robcox2910.github.io/pygit/) |
|
|
241
|
+
| PySearch | Search engines | [pysearch](https://github.com/robcox2910/pysearch) | [Docs](https://robcox2910.github.io/pysearch/) |
|
|
242
|
+
| PyMQ | Message queues | [pymq](https://github.com/robcox2910/pymq) | [Docs](https://robcox2910.github.io/pymq/) |
|
|
243
|
+
| PyLLM | Language models (the brain) | [pyllm](https://github.com/robcox2910/pyllm) | [Docs](https://robcox2910.github.io/pyllm/) |
|
|
244
|
+
| PyScheduler | Job scheduling | [pyscheduler](https://github.com/robcox2910/pyscheduler) | [Docs](https://robcox2910.github.io/pyscheduler/) |
|
|
245
|
+
| PyStack | Full-stack integration | [pystack](https://github.com/robcox2910/pystack) | [Docs](https://robcox2910.github.io/pystack/) |
|
|
246
|
+
<!-- series:end -->
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT -- see [LICENSE](LICENSE) for details.
|