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.
Files changed (125) hide show
  1. fromscratch_pynet-0.2.0/.github/workflows/ci.yml +14 -0
  2. fromscratch_pynet-0.2.0/.github/workflows/docs.yml +16 -0
  3. fromscratch_pynet-0.2.0/.github/workflows/release.yml +58 -0
  4. fromscratch_pynet-0.2.0/.gitignore +14 -0
  5. fromscratch_pynet-0.2.0/.pre-commit-config.yaml +56 -0
  6. fromscratch_pynet-0.2.0/CLAUDE.md +53 -0
  7. fromscratch_pynet-0.2.0/CONTRIBUTING.md +199 -0
  8. fromscratch_pynet-0.2.0/LICENSE +21 -0
  9. fromscratch_pynet-0.2.0/PKG-INFO +250 -0
  10. fromscratch_pynet-0.2.0/README.md +217 -0
  11. fromscratch_pynet-0.2.0/conftest.py +21 -0
  12. fromscratch_pynet-0.2.0/docs/concepts/base64.md +63 -0
  13. fromscratch_pynet-0.2.0/docs/concepts/cancellation-and-timeouts.md +150 -0
  14. fromscratch_pynet-0.2.0/docs/concepts/dns.md +51 -0
  15. fromscratch_pynet-0.2.0/docs/concepts/event-loop.md +70 -0
  16. fromscratch_pynet-0.2.0/docs/concepts/framing.md +109 -0
  17. fromscratch_pynet-0.2.0/docs/concepts/ftp.md +52 -0
  18. fromscratch_pynet-0.2.0/docs/concepts/heartbeats-and-reconnect.md +398 -0
  19. fromscratch_pynet-0.2.0/docs/concepts/http-client.md +80 -0
  20. fromscratch_pynet-0.2.0/docs/concepts/json-client.md +44 -0
  21. fromscratch_pynet-0.2.0/docs/concepts/messaging.md +281 -0
  22. fromscratch_pynet-0.2.0/docs/concepts/our-loop-vs-asyncio.md +164 -0
  23. fromscratch_pynet-0.2.0/docs/concepts/ping.md +56 -0
  24. fromscratch_pynet-0.2.0/docs/concepts/self-pipe-trick.md +65 -0
  25. fromscratch_pynet-0.2.0/docs/concepts/smtp.md +58 -0
  26. fromscratch_pynet-0.2.0/docs/concepts/streams.md +160 -0
  27. fromscratch_pynet-0.2.0/docs/concepts/subprocesses.md +169 -0
  28. fromscratch_pynet-0.2.0/docs/concepts/sync-primitives.md +86 -0
  29. fromscratch_pynet-0.2.0/docs/concepts/tasks-and-await.md +154 -0
  30. fromscratch_pynet-0.2.0/docs/concepts/tcp.md +52 -0
  31. fromscratch_pynet-0.2.0/docs/concepts/threads.md +73 -0
  32. fromscratch_pynet-0.2.0/docs/concepts/timers.md +104 -0
  33. fromscratch_pynet-0.2.0/docs/concepts/tls.md +73 -0
  34. fromscratch_pynet-0.2.0/docs/concepts/udp.md +46 -0
  35. fromscratch_pynet-0.2.0/docs/concepts/urls.md +60 -0
  36. fromscratch_pynet-0.2.0/docs/concepts/websocket-real.md +357 -0
  37. fromscratch_pynet-0.2.0/docs/concepts/websocket.md +66 -0
  38. fromscratch_pynet-0.2.0/docs/index.md +114 -0
  39. fromscratch_pynet-0.2.0/docs/property-testing.md +349 -0
  40. fromscratch_pynet-0.2.0/docs/superpowers/plans/2026-10-16-pynet-event-loop.md +656 -0
  41. fromscratch_pynet-0.2.0/docs/superpowers/specs/2026-10-16-pynet-event-loop-design.md +365 -0
  42. fromscratch_pynet-0.2.0/examples/chat_room.py +107 -0
  43. fromscratch_pynet-0.2.0/examples/echo_server.py +65 -0
  44. fromscratch_pynet-0.2.0/examples/heartbeat_demo.py +89 -0
  45. fromscratch_pynet-0.2.0/examples/job_runner.py +95 -0
  46. fromscratch_pynet-0.2.0/examples/ws_echo.py +44 -0
  47. fromscratch_pynet-0.2.0/mkdocs.yml +101 -0
  48. fromscratch_pynet-0.2.0/overrides/main.html +7 -0
  49. fromscratch_pynet-0.2.0/pyproject.toml +162 -0
  50. fromscratch_pynet-0.2.0/src/pynet/__init__.py +1 -0
  51. fromscratch_pynet-0.2.0/src/pynet/_digits.py +16 -0
  52. fromscratch_pynet-0.2.0/src/pynet/_version.py +24 -0
  53. fromscratch_pynet-0.2.0/src/pynet/base64.py +138 -0
  54. fromscratch_pynet-0.2.0/src/pynet/cli.py +28 -0
  55. fromscratch_pynet-0.2.0/src/pynet/dns.py +79 -0
  56. fromscratch_pynet-0.2.0/src/pynet/errors.py +88 -0
  57. fromscratch_pynet-0.2.0/src/pynet/framing.py +171 -0
  58. fromscratch_pynet-0.2.0/src/pynet/ftp.py +54 -0
  59. fromscratch_pynet-0.2.0/src/pynet/http.py +271 -0
  60. fromscratch_pynet-0.2.0/src/pynet/json_client.py +82 -0
  61. fromscratch_pynet-0.2.0/src/pynet/loop/__init__.py +46 -0
  62. fromscratch_pynet-0.2.0/src/pynet/loop/children.py +351 -0
  63. fromscratch_pynet-0.2.0/src/pynet/loop/control.py +237 -0
  64. fromscratch_pynet-0.2.0/src/pynet/loop/futures.py +187 -0
  65. fromscratch_pynet-0.2.0/src/pynet/loop/reactor.py +502 -0
  66. fromscratch_pynet-0.2.0/src/pynet/loop/sync.py +295 -0
  67. fromscratch_pynet-0.2.0/src/pynet/loop/tasks.py +296 -0
  68. fromscratch_pynet-0.2.0/src/pynet/loop/testing.py +37 -0
  69. fromscratch_pynet-0.2.0/src/pynet/loop/threads.py +164 -0
  70. fromscratch_pynet-0.2.0/src/pynet/messaging/__init__.py +92 -0
  71. fromscratch_pynet-0.2.0/src/pynet/messaging/codec.py +244 -0
  72. fromscratch_pynet-0.2.0/src/pynet/messaging/peer.py +659 -0
  73. fromscratch_pynet-0.2.0/src/pynet/messaging/reconnect.py +338 -0
  74. fromscratch_pynet-0.2.0/src/pynet/ping.py +64 -0
  75. fromscratch_pynet-0.2.0/src/pynet/process.py +236 -0
  76. fromscratch_pynet-0.2.0/src/pynet/py.typed +0 -0
  77. fromscratch_pynet-0.2.0/src/pynet/smtp.py +76 -0
  78. fromscratch_pynet-0.2.0/src/pynet/streams.py +861 -0
  79. fromscratch_pynet-0.2.0/src/pynet/tcp.py +129 -0
  80. fromscratch_pynet-0.2.0/src/pynet/tls.py +81 -0
  81. fromscratch_pynet-0.2.0/src/pynet/udp.py +53 -0
  82. fromscratch_pynet-0.2.0/src/pynet/url.py +117 -0
  83. fromscratch_pynet-0.2.0/src/pynet/websocket.py +90 -0
  84. fromscratch_pynet-0.2.0/src/pynet/ws/__init__.py +81 -0
  85. fromscratch_pynet-0.2.0/src/pynet/ws/connection.py +393 -0
  86. fromscratch_pynet-0.2.0/src/pynet/ws/endpoints.py +200 -0
  87. fromscratch_pynet-0.2.0/src/pynet/ws/frames.py +257 -0
  88. fromscratch_pynet-0.2.0/src/pynet/ws/handshake.py +206 -0
  89. fromscratch_pynet-0.2.0/tests/__init__.py +1 -0
  90. fromscratch_pynet-0.2.0/tests/loop/__init__.py +1 -0
  91. fromscratch_pynet-0.2.0/tests/loop/test_control.py +425 -0
  92. fromscratch_pynet-0.2.0/tests/loop/test_io.py +242 -0
  93. fromscratch_pynet-0.2.0/tests/loop/test_reactor.py +399 -0
  94. fromscratch_pynet-0.2.0/tests/loop/test_sync.py +402 -0
  95. fromscratch_pynet-0.2.0/tests/loop/test_tasks.py +782 -0
  96. fromscratch_pynet-0.2.0/tests/loop/test_threads.py +302 -0
  97. fromscratch_pynet-0.2.0/tests/pretend_pipe.py +164 -0
  98. fromscratch_pynet-0.2.0/tests/strategies.py +117 -0
  99. fromscratch_pynet-0.2.0/tests/test_base64.py +126 -0
  100. fromscratch_pynet-0.2.0/tests/test_digits.py +29 -0
  101. fromscratch_pynet-0.2.0/tests/test_dns.py +75 -0
  102. fromscratch_pynet-0.2.0/tests/test_examples.py +46 -0
  103. fromscratch_pynet-0.2.0/tests/test_framing.py +271 -0
  104. fromscratch_pynet-0.2.0/tests/test_ftp.py +89 -0
  105. fromscratch_pynet-0.2.0/tests/test_heartbeat.py +452 -0
  106. fromscratch_pynet-0.2.0/tests/test_http.py +266 -0
  107. fromscratch_pynet-0.2.0/tests/test_json_client.py +53 -0
  108. fromscratch_pynet-0.2.0/tests/test_messaging_codec.py +311 -0
  109. fromscratch_pynet-0.2.0/tests/test_model.py +141 -0
  110. fromscratch_pynet-0.2.0/tests/test_peer.py +893 -0
  111. fromscratch_pynet-0.2.0/tests/test_ping.py +55 -0
  112. fromscratch_pynet-0.2.0/tests/test_process.py +589 -0
  113. fromscratch_pynet-0.2.0/tests/test_properties.py +360 -0
  114. fromscratch_pynet-0.2.0/tests/test_reconnect.py +584 -0
  115. fromscratch_pynet-0.2.0/tests/test_smtp.py +91 -0
  116. fromscratch_pynet-0.2.0/tests/test_streams.py +758 -0
  117. fromscratch_pynet-0.2.0/tests/test_tcp.py +120 -0
  118. fromscratch_pynet-0.2.0/tests/test_tls.py +103 -0
  119. fromscratch_pynet-0.2.0/tests/test_udp.py +59 -0
  120. fromscratch_pynet-0.2.0/tests/test_url.py +90 -0
  121. fromscratch_pynet-0.2.0/tests/test_websocket.py +98 -0
  122. fromscratch_pynet-0.2.0/tests/test_ws_connection.py +707 -0
  123. fromscratch_pynet-0.2.0/tests/test_ws_frames.py +221 -0
  124. fromscratch_pynet-0.2.0/tests/test_ws_handshake.py +189 -0
  125. fromscratch_pynet-0.2.0/uv.lock +934 -0
@@ -0,0 +1,14 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ ci:
14
+ uses: robcox2910/robcox2910.github.io/.github/workflows/python-ci.yml@v1
@@ -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,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ htmlcov/
9
+ .coverage
10
+ .pyright/
11
+ .DS_Store
12
+ src/pynet/_version.py
13
+ site/
14
+ .hypothesis/
@@ -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.