one-ring-loop 0.1.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 (43) hide show
  1. one_ring_loop-0.1.0/.gitignore +43 -0
  2. one_ring_loop-0.1.0/.python-version +1 -0
  3. one_ring_loop-0.1.0/CLAUDE.md +28 -0
  4. one_ring_loop-0.1.0/PKG-INFO +83 -0
  5. one_ring_loop-0.1.0/README.md +60 -0
  6. one_ring_loop-0.1.0/justfile +20 -0
  7. one_ring_loop-0.1.0/pyproject.toml +44 -0
  8. one_ring_loop-0.1.0/src/one_ring_loop/__init__.py +12 -0
  9. one_ring_loop-0.1.0/src/one_ring_loop/_utils.py +63 -0
  10. one_ring_loop-0.1.0/src/one_ring_loop/cancellation.py +49 -0
  11. one_ring_loop-0.1.0/src/one_ring_loop/exceptions.py +2 -0
  12. one_ring_loop-0.1.0/src/one_ring_loop/fileio/__init__.py +40 -0
  13. one_ring_loop-0.1.0/src/one_ring_loop/log.py +56 -0
  14. one_ring_loop-0.1.0/src/one_ring_loop/loop.py +246 -0
  15. one_ring_loop-0.1.0/src/one_ring_loop/lowlevel.py +36 -0
  16. one_ring_loop-0.1.0/src/one_ring_loop/operations.py +29 -0
  17. one_ring_loop-0.1.0/src/one_ring_loop/py.typed +0 -0
  18. one_ring_loop-0.1.0/src/one_ring_loop/socketio/__init__.py +108 -0
  19. one_ring_loop-0.1.0/src/one_ring_loop/streams/__init__.py +0 -0
  20. one_ring_loop-0.1.0/src/one_ring_loop/streams/buffered.py +97 -0
  21. one_ring_loop-0.1.0/src/one_ring_loop/streams/exceptions.py +14 -0
  22. one_ring_loop-0.1.0/src/one_ring_loop/streams/memory.py +199 -0
  23. one_ring_loop-0.1.0/src/one_ring_loop/streams/protocols.py +29 -0
  24. one_ring_loop-0.1.0/src/one_ring_loop/streams/tls.py +126 -0
  25. one_ring_loop-0.1.0/src/one_ring_loop/sync_primitives.py +139 -0
  26. one_ring_loop-0.1.0/src/one_ring_loop/task/__init__.py +383 -0
  27. one_ring_loop-0.1.0/src/one_ring_loop/task/state.py +35 -0
  28. one_ring_loop-0.1.0/src/one_ring_loop/timerio/__init__.py +17 -0
  29. one_ring_loop-0.1.0/src/one_ring_loop/typedefs.py +18 -0
  30. one_ring_loop-0.1.0/tests/conftest.py +17 -0
  31. one_ring_loop-0.1.0/tests/streams/__init__.py +0 -0
  32. one_ring_loop-0.1.0/tests/streams/conftest.py +1 -0
  33. one_ring_loop-0.1.0/tests/streams/test_buffered_byte_stream.py +92 -0
  34. one_ring_loop-0.1.0/tests/streams/test_memory_object_stream.py +168 -0
  35. one_ring_loop-0.1.0/tests/streams/test_tls_stream.py +91 -0
  36. one_ring_loop-0.1.0/tests/test_cancellation.py +94 -0
  37. one_ring_loop-0.1.0/tests/test_fileio.py +27 -0
  38. one_ring_loop-0.1.0/tests/test_log.py +37 -0
  39. one_ring_loop-0.1.0/tests/test_loop.py +10 -0
  40. one_ring_loop-0.1.0/tests/test_socketio.py +54 -0
  41. one_ring_loop-0.1.0/tests/test_sync_primitives.py +253 -0
  42. one_ring_loop-0.1.0/tests/test_taskgroup.py +117 -0
  43. one_ring_loop-0.1.0/tests/test_timerio.py +13 -0
@@ -0,0 +1,43 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+
8
+ # Virtual environments
9
+ .venv/
10
+
11
+ # Testing
12
+ .coverage
13
+ htmlcov/
14
+ .pytest_cache/
15
+
16
+ # Tools
17
+ .ruff_cache/
18
+
19
+ # IDE
20
+ .idea/
21
+ .vscode/*
22
+ !.vscode/settings.json
23
+ !.vscode/extensions.json
24
+ *.swp
25
+ *.swo
26
+
27
+ # Docs
28
+ site/
29
+
30
+ # Secrets
31
+ .env
32
+ .env.*
33
+ *.pem
34
+
35
+ # OS
36
+ .DS_Store
37
+ Thumbs.db
38
+
39
+ # Personal testing.
40
+ tmp/
41
+
42
+ # Agents
43
+ .claude
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,28 @@
1
+ # one-ring-loop — Package Context
2
+
3
+ Part of the **one-ring** monorepo. See the root `CLAUDE.md` for shared conventions.
4
+
5
+ ## Purpose
6
+
7
+ <!-- Describe what this package does -->
8
+
9
+ ## Package Commands
10
+
11
+ ```bash
12
+ # From this directory:
13
+ just test # Run tests for this package
14
+ just test-cov # Tests with coverage
15
+ just typecheck # Type check this package
16
+
17
+ # From monorepo root:
18
+ just test-pkg one-ring-loop # Test this package
19
+ just check # Run all checks (all packages)
20
+ ```
21
+
22
+ ## Layout
23
+
24
+ ```
25
+ src/one_ring_loop/ # Source code
26
+ tests/ # Tests
27
+ pyproject.toml # Package metadata (tool config inherited from root)
28
+ ```
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: one-ring-loop
3
+ Version: 0.1.0
4
+ Summary: Custom async event loop built on io_uring with structured concurrency, file/socket IO, and timers
5
+ Project-URL: Homepage, https://github.com/otto-sellerstam/one-ring
6
+ Project-URL: Repository, https://github.com/otto-sellerstam/one-ring
7
+ Project-URL: Issues, https://github.com/otto-sellerstam/one-ring/issues
8
+ Author-email: Otto Sellerstam <ottosellerstam@gmail.com>
9
+ License: MIT
10
+ Keywords: async,coroutines,event-loop,io_uring,linux
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: System :: Networking
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.14
20
+ Requires-Dist: one-ring-core>=0.1.0
21
+ Requires-Dist: structlog>=24.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # one-ring-loop
25
+
26
+ Custom async event loop built on [one-ring-core](https://pypi.org/project/one-ring-core/) and Linux io_uring.
27
+
28
+ Part of the [one-ring](https://github.com/otto-sellerstam/one-ring) project.
29
+
30
+ ## What it provides
31
+
32
+ - **Task scheduling** with `Task` and `TaskGroup` (structured concurrency)
33
+ - **File IO** - async file operations via io_uring
34
+ - **Socket IO** - TCP server/client with `create_server`, `Connection.receive`/`send`
35
+ - **Timers** - async `sleep` backed by io_uring timeouts
36
+ - **Streams** - buffered byte streams, TLS wrapping, memory streams
37
+ - **Cancellation** - `move_on_after`/`fail_after` scoped timeout/cancellation
38
+
39
+ ## Example
40
+
41
+ ```python
42
+ from one_ring_loop import TaskGroup, run
43
+ from one_ring_loop.socketio import Connection, create_server
44
+
45
+ def echo_handler(conn):
46
+ try:
47
+ while True:
48
+ data = yield from conn.receive(1024)
49
+ if not data:
50
+ break
51
+ yield from conn.send(b"Echo: " + data)
52
+ finally:
53
+ yield from conn.close()
54
+
55
+ def main():
56
+ server = yield from create_server(b"0.0.0.0", 9999)
57
+ tg = TaskGroup()
58
+ tg.enter()
59
+ try:
60
+ while True:
61
+ conn = yield from server.accept()
62
+ tg.create_task(echo_handler(conn))
63
+ finally:
64
+ yield from tg.exit()
65
+ yield from server.close()
66
+
67
+ run(main())
68
+ ```
69
+
70
+ ## Requirements
71
+
72
+ - **Linux** with io_uring support (kernel 6.7+)
73
+ - **Python 3.14+**
74
+
75
+ ## Installation
76
+
77
+ ```bash
78
+ uv add one-ring-loop
79
+ ```
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,60 @@
1
+ # one-ring-loop
2
+
3
+ Custom async event loop built on [one-ring-core](https://pypi.org/project/one-ring-core/) and Linux io_uring.
4
+
5
+ Part of the [one-ring](https://github.com/otto-sellerstam/one-ring) project.
6
+
7
+ ## What it provides
8
+
9
+ - **Task scheduling** with `Task` and `TaskGroup` (structured concurrency)
10
+ - **File IO** - async file operations via io_uring
11
+ - **Socket IO** - TCP server/client with `create_server`, `Connection.receive`/`send`
12
+ - **Timers** - async `sleep` backed by io_uring timeouts
13
+ - **Streams** - buffered byte streams, TLS wrapping, memory streams
14
+ - **Cancellation** - `move_on_after`/`fail_after` scoped timeout/cancellation
15
+
16
+ ## Example
17
+
18
+ ```python
19
+ from one_ring_loop import TaskGroup, run
20
+ from one_ring_loop.socketio import Connection, create_server
21
+
22
+ def echo_handler(conn):
23
+ try:
24
+ while True:
25
+ data = yield from conn.receive(1024)
26
+ if not data:
27
+ break
28
+ yield from conn.send(b"Echo: " + data)
29
+ finally:
30
+ yield from conn.close()
31
+
32
+ def main():
33
+ server = yield from create_server(b"0.0.0.0", 9999)
34
+ tg = TaskGroup()
35
+ tg.enter()
36
+ try:
37
+ while True:
38
+ conn = yield from server.accept()
39
+ tg.create_task(echo_handler(conn))
40
+ finally:
41
+ yield from tg.exit()
42
+ yield from server.close()
43
+
44
+ run(main())
45
+ ```
46
+
47
+ ## Requirements
48
+
49
+ - **Linux** with io_uring support (kernel 6.7+)
50
+ - **Python 3.14+**
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ uv add one-ring-loop
56
+ ```
57
+
58
+ ## License
59
+
60
+ MIT
@@ -0,0 +1,20 @@
1
+ # List available recipes
2
+ default:
3
+ @just --list
4
+
5
+ pkg := "one_ring_loop"
6
+
7
+ # Run tests for this package
8
+ test *args:
9
+ uv run pytest tests/ {{args}}
10
+
11
+ # Run tests with coverage
12
+ test-cov:
13
+ uv run pytest tests/ --cov={{pkg}} --cov-report=term-missing
14
+
15
+ # Run type checker for this package
16
+ typecheck:
17
+ uv run pyrefly check src/ tests/
18
+
19
+ # Run all checks for this package
20
+ check: typecheck test
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "one-ring-loop"
3
+ version = "0.1.0"
4
+ description = "Custom async event loop built on io_uring with structured concurrency, file/socket IO, and timers"
5
+ requires-python = ">=3.14"
6
+ license = { text = "MIT" }
7
+ authors = [
8
+ { name = "Otto Sellerstam", email = "ottosellerstam@gmail.com" },
9
+ ]
10
+ readme = "README.md"
11
+ keywords = ["io_uring", "async", "event-loop", "coroutines", "linux"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: POSIX :: Linux",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.14",
19
+ "Topic :: System :: Networking",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "one-ring-core>=0.1.0",
24
+ "structlog>=24.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/otto-sellerstam/one-ring"
29
+ Repository = "https://github.com/otto-sellerstam/one-ring"
30
+ Issues = "https://github.com/otto-sellerstam/one-ring/issues"
31
+
32
+ [build-system]
33
+ requires = ["hatchling"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/one_ring_loop"]
38
+
39
+ # ── Coverage (per-package source) ───────────────────────────────────────
40
+ # Other coverage settings (fail_under, exclude_lines) are in root pyproject.toml.
41
+
42
+ [tool.coverage.run]
43
+ source = ["one_ring_loop"]
44
+ branch = true
@@ -0,0 +1,12 @@
1
+ """One Ring Loop package."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from one_ring_loop.loop import run
6
+ from one_ring_loop.task import Task, TaskGroup
7
+
8
+ __all__ = [
9
+ "Task",
10
+ "TaskGroup",
11
+ "run",
12
+ ]
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from collections import deque
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING, cast
7
+
8
+ from one_ring_core.results import IOResult
9
+
10
+ if TYPE_CHECKING:
11
+ from one_ring_core.operations import IOOperation
12
+ from one_ring_loop.loop import Loop
13
+ from one_ring_loop.typedefs import Coro, TaskID
14
+
15
+
16
+ def _get_new_operation_id() -> TaskID:
17
+ """Gets an unused ID to submit to the IO worker."""
18
+ ret = _local.free_operation_id
19
+ _local.free_operation_id += 1
20
+
21
+ return ret
22
+
23
+
24
+ def _execute[T: IOResult](op: IOOperation[T]) -> Coro[T]:
25
+ """Unwrap an IO completion into the expected result type."""
26
+ expected = op.result_type
27
+ completion = yield cast("IOOperation[IOResult]", op)
28
+ if completion is not None and isinstance(result := completion.unwrap(), expected):
29
+ return result
30
+ elif completion is None:
31
+ raise RuntimeError("Low level coroutine was sent None")
32
+
33
+ msg = f"Expected {expected.__name__}, got {type(completion)}. Expected {expected}"
34
+ raise TypeError(msg)
35
+
36
+
37
+ @dataclass(kw_only=True)
38
+ class _Local(threading.local):
39
+ """Wrapper around threading.local for proper type annotations."""
40
+
41
+ loop: Loop | None = None
42
+ free_operation_id: int = 1
43
+
44
+ # TODO: Move the below two to be attributes on Loop.
45
+ cancel_queue: deque[TaskID] = field(default_factory=deque)
46
+ unpark_queue: deque[TaskID] = field(default_factory=deque)
47
+
48
+ def cleanup(self) -> None:
49
+ """Resets all attributes."""
50
+ self.loop = None
51
+ self.free_operation_id = 1
52
+
53
+ self.cancel_queue = deque()
54
+ self.unpark_queue = deque()
55
+
56
+
57
+ _local = _Local()
58
+
59
+ __all__ = [
60
+ "_execute",
61
+ "_get_new_operation_id",
62
+ "_local",
63
+ ]
@@ -0,0 +1,49 @@
1
+ from contextlib import contextmanager, suppress
2
+ from typing import TYPE_CHECKING
3
+
4
+ from one_ring_loop.exceptions import Cancelled
5
+ from one_ring_loop.log import get_logger
6
+ from one_ring_loop.task import CancelScope, _create_standalone_task
7
+ from one_ring_loop.timerio import sleep
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Generator
11
+
12
+ from one_ring_loop.typedefs import Coro
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ @contextmanager
18
+ def fail_after(delay: float, *, shield: bool = False) -> Generator[CancelScope]:
19
+ """Cancels cancel scope and throws Cancelled after delay."""
20
+
21
+ def cancellation_task(cancel_scope: CancelScope) -> Coro[None]:
22
+ """Background task that sleeps for delay.
23
+
24
+ Cancels the cancel scope if not finished after sleep.
25
+ """
26
+ with suppress(Cancelled):
27
+ yield from sleep(delay)
28
+
29
+ if not finished:
30
+ cancel_scope.cancel()
31
+
32
+ finished = False
33
+ with CancelScope(shielded=shield) as scope:
34
+ task = _create_standalone_task(cancellation_task(scope), None, None)
35
+ yield scope
36
+ task.current_cancel_scope().cancel()
37
+ finished = True
38
+
39
+
40
+ @contextmanager
41
+ def move_on_after(delay: float, *, shield: bool = False) -> Generator[CancelScope]:
42
+ """Moves on after delay by catching Cancelled."""
43
+ with fail_after(delay, shield=shield) as cancel_scope:
44
+ try:
45
+ yield cancel_scope
46
+ except Cancelled:
47
+ if not cancel_scope.cancelled:
48
+ # Another scope cancelled from above. Re-raise.
49
+ raise
@@ -0,0 +1,2 @@
1
+ class Cancelled(BaseException):
2
+ """Thrown when coroutine is cancelled, like asyncio.CancelledError."""
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ from typing import TYPE_CHECKING
4
+
5
+ from one_ring_core.operations import Close, FileOpen, FileRead, FileWrite
6
+ from one_ring_loop._utils import _execute
7
+
8
+ if TYPE_CHECKING:
9
+ from one_ring_loop.typedefs import Coro
10
+
11
+
12
+ @dataclass(slots=True, kw_only=True)
13
+ class File:
14
+ """Utility wrapper for file operations. Represents a single regular file."""
15
+
16
+ fd: int
17
+
18
+ def read(self) -> Coro[str]:
19
+ """Read file low-level coroutine."""
20
+ result = yield from _execute(FileRead(fd=self.fd))
21
+ return result.content.decode()
22
+
23
+ def write(self, data: bytes | str) -> Coro[int]:
24
+ """Write file low-level coroutine."""
25
+ _data = data.encode() if isinstance(data, str) else data
26
+ result = yield from _execute(FileWrite(fd=self.fd, data=_data))
27
+ return result.size
28
+
29
+ def close(self) -> Coro[None]:
30
+ """Close file low-level coroutine."""
31
+ yield from _execute(Close(fd=self.fd))
32
+ return None
33
+
34
+
35
+ def open_file(path: str | Path, mode: str = "r") -> Coro[File]:
36
+ """Open file coroutine."""
37
+ _path = str(path) if isinstance(path, Path) else path
38
+
39
+ result = yield from _execute(FileOpen(path=_path.encode(), mode=mode))
40
+ return File(fd=result.fd)
@@ -0,0 +1,56 @@
1
+ """Structured logging configuration using structlog."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+
8
+ import structlog
9
+
10
+
11
+ def setup_logging() -> None:
12
+ """Configure structlog with sensible defaults.
13
+
14
+ Uses JSON output when ``LOG_FORMAT=json`` (e.g. production),
15
+ otherwise uses colored console output for development.
16
+ """
17
+ shared_processors: list[structlog.types.Processor] = [
18
+ structlog.contextvars.merge_contextvars,
19
+ structlog.stdlib.add_log_level,
20
+ # structlog.stdlib.add_logger_name,
21
+ structlog.processors.TimeStamper(fmt="iso"),
22
+ structlog.processors.StackInfoRenderer(),
23
+ structlog.processors.UnicodeDecoder(),
24
+ ]
25
+
26
+ if os.environ.get("LOG_FORMAT") == "json":
27
+ renderer: structlog.types.Processor = structlog.processors.JSONRenderer()
28
+ else:
29
+ renderer = structlog.dev.ConsoleRenderer()
30
+
31
+ structlog.configure(
32
+ processors=[
33
+ *shared_processors,
34
+ renderer,
35
+ ],
36
+ wrapper_class=structlog.make_filtering_bound_logger(0),
37
+ context_class=dict,
38
+ logger_factory=structlog.PrintLoggerFactory(file=sys.stderr),
39
+ cache_logger_on_first_use=True,
40
+ )
41
+
42
+
43
+ _configured = False
44
+
45
+
46
+ def get_logger(*args: object, **kwargs: object) -> structlog.stdlib.BoundLogger:
47
+ """Return a structlog logger, configuring on first call.
48
+
49
+ This avoids running ``setup_logging()`` at import time, which would
50
+ interfere with tests and multi-process setups.
51
+ """
52
+ global _configured # noqa: PLW0603
53
+ if not _configured:
54
+ setup_logging()
55
+ _configured = True
56
+ return structlog.get_logger(*args, **kwargs)