[object Object]

← back to Exo

Scaffold Event Sourcing

5abf03e31b01d3326a75c0b47654d0814a229fb4 · 2025-06-29 19:44:58 +0100 · Arbion Halili

Files touched

Diff

commit 5abf03e31b01d3326a75c0b47654d0814a229fb4
Author: Arbion Halili <99731180+ToxicPine@users.noreply.github.com>
Date:   Sun Jun 29 19:44:58 2025 +0100

    Scaffold Event Sourcing
---
 master/idempotency.py                 |  26 ++++++
 networking/.gitignore                 |   1 +
 networking/Cargo.lock                 | 171 ++++++++++++++++++++++++++++++++++
 networking/Cargo.toml                 |  14 +++
 networking/README.md                  |   0
 networking/pyproject.toml             |  22 +++++
 networking/src/lib.rs                 |  15 +++
 networking/src/networking/__init__.py |   5 +
 networking/src/networking/_core.pyi   |   1 +
 pyproject.toml                        |   8 +-
 shared/constants.py                   |  11 +++
 shared/env.py                         |  15 ++-
 shared/pyproject.toml                 |   3 +-
 shared/types/event_sourcing.py        |  99 ++++++++++++++++++++
 uv.lock                               |  36 +++++++
 15 files changed, 423 insertions(+), 4 deletions(-)

diff --git a/master/idempotency.py b/master/idempotency.py
new file mode 100644
index 00000000..661d1e44
--- /dev/null
+++ b/master/idempotency.py
@@ -0,0 +1,26 @@
+from hashlib import sha3_224 as hasher
+from typing import Sequence, TypeVar
+
+from shared.types.event_sourcing import EventId, EventTypes, IdemKeyGenerator, State
+
+EventTypeT = TypeVar("EventTypeT", bound=EventTypes)
+
+
+def get_idem_tag_generator(base: str) -> IdemKeyGenerator[EventTypeT]:
+    """Generates idempotency keys for events.
+
+    The keys are generated by hashing the state sequence number against a base string.
+    You can pick any base string, **so long as it's not used in any other function that generates idempotency keys**.
+    """
+
+    def get_idem_keys(state: State[EventTypeT], num_keys: int) -> Sequence[EventId]:
+        def recurse(n: int, last: bytes) -> Sequence[EventId]:
+            if n == 0:
+                return []
+            next_hash = hasher(last).digest()
+            return (EventId(next_hash.hex()), *recurse(n - 1, next_hash))
+
+        initial_bytes = state.sequence_number.to_bytes(8, byteorder="big", signed=False)
+        return recurse(num_keys, initial_bytes)
+
+    return get_idem_keys
diff --git a/networking/.gitignore b/networking/.gitignore
new file mode 100644
index 00000000..9f970225
--- /dev/null
+++ b/networking/.gitignore
@@ -0,0 +1 @@
+target/
\ No newline at end of file
diff --git a/networking/Cargo.lock b/networking/Cargo.lock
new file mode 100644
index 00000000..328ad73a
--- /dev/null
+++ b/networking/Cargo.lock
@@ -0,0 +1,171 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "indoc"
+version = "2.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "networking"
+version = "0.1.0"
+dependencies = [
+ "pyo3",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.22.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884"
+dependencies = [
+ "cfg-if",
+ "indoc",
+ "libc",
+ "memoffset",
+ "once_cell",
+ "portable-atomic",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.22.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38"
+dependencies = [
+ "once_cell",
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.22.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.22.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.22.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "pyo3-build-config",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unindent"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
diff --git a/networking/Cargo.toml b/networking/Cargo.toml
new file mode 100644
index 00000000..6e458e40
--- /dev/null
+++ b/networking/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "networking"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+name = "_core"
+# "cdylib" is necessary to produce a shared library for Python to import from.
+crate-type = ["cdylib"]
+
+[dependencies]
+# "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so)
+# "abi3-py39" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.9
+pyo3 = { version = "0.22.4", features = ["extension-module", "abi3-py39"] }
diff --git a/networking/README.md b/networking/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/networking/pyproject.toml b/networking/pyproject.toml
new file mode 100644
index 00000000..b2f433b7
--- /dev/null
+++ b/networking/pyproject.toml
@@ -0,0 +1,22 @@
+[project]
+name = "exo-networking"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+authors = [
+    { name = "Arbion Halili", email = "99731180+ToxicPine@users.noreply.github.com" }
+]
+requires-python = ">=3.13"
+dependencies = []
+
+[project.scripts]
+networking = "networking:main"
+
+[tool.maturin]
+module-name = "networking._core"
+python-packages = ["networking"]
+python-source = "src"
+
+[build-system]
+requires = ["maturin>=1.0,<2.0"]
+build-backend = "maturin"
diff --git a/networking/src/lib.rs b/networking/src/lib.rs
new file mode 100644
index 00000000..915d8a39
--- /dev/null
+++ b/networking/src/lib.rs
@@ -0,0 +1,15 @@
+use pyo3::prelude::*;
+
+#[pyfunction]
+fn hello_from_bin() -> String {
+    "Hello from networking!".to_string()
+}
+
+/// A Python module implemented in Rust. The name of this function must match
+/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
+/// import the module.
+#[pymodule]
+fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
+    m.add_function(wrap_pyfunction!(hello_from_bin, m)?)?;
+    Ok(())
+}
diff --git a/networking/src/networking/__init__.py b/networking/src/networking/__init__.py
new file mode 100644
index 00000000..e357cd98
--- /dev/null
+++ b/networking/src/networking/__init__.py
@@ -0,0 +1,5 @@
+from networking._core import hello_from_bin
+
+
+def main() -> None:
+    print(hello_from_bin())
diff --git a/networking/src/networking/_core.pyi b/networking/src/networking/_core.pyi
new file mode 100644
index 00000000..d52129eb
--- /dev/null
+++ b/networking/src/networking/_core.pyi
@@ -0,0 +1 @@
+def hello_from_bin() -> str: ...
diff --git a/pyproject.toml b/pyproject.toml
index bfd0708b..73dca1bf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,6 +13,7 @@ dependencies = [
 [dependency-groups]
 dev = [
     "basedpyright>=1.29.4",
+    "maturin>=1.9.0",
     "pytest>=8.4.0",
     "ruff>=0.11.13",
 ]
@@ -29,7 +30,11 @@ darwin = [
 
 [tool.uv.workspace]
 members = [
-    "master", "worker", "shared", "engines/*",
+    "master",
+    "worker",
+    "shared",
+    "engines/*",
+    "networking",
 ]
 
 [tool.uv.sources]
@@ -37,6 +42,7 @@ exo-shared = { workspace = true }
 exo-master = { workspace = true }
 exo-worker = { workspace = true }
 exo-engine-mlx = { workspace = true }
+exo-networking = { workspace = true }
 
 [build-system]
 requires = ["hatchling"]
diff --git a/shared/constants.py b/shared/constants.py
new file mode 100644
index 00000000..5410f899
--- /dev/null
+++ b/shared/constants.py
@@ -0,0 +1,11 @@
+from pathlib import Path
+
+EXO_HOME = Path.home() / ".exo"
+EXO_EVENT_DB = EXO_HOME / "event_db.sqlite3"
+EXO_MASTER_CONFIG = EXO_HOME / "master.json"
+EXO_WORKER_CONFIG = EXO_HOME / "worker.json"
+EXO_MASTER_LOG = EXO_HOME / "master.log"
+EXO_WORKER_LOG = EXO_HOME / "worker.log"
+
+EXO_WORKER_KEYRING_FILE = EXO_HOME / "worker_keyring"
+EXO_MASTER_KEYRING_FILE = EXO_HOME / "master_keyring"
diff --git a/shared/env.py b/shared/env.py
index 23e39704..c87cf094 100644
--- a/shared/env.py
+++ b/shared/env.py
@@ -2,9 +2,20 @@ import logging
 import os
 from typing import TypeVar
 
-from pydantic import BaseModel, ValidationError
+from pydantic import BaseModel, ConfigDict, ValidationError
 
-EnvSchema = TypeVar("EnvSchema", bound=BaseModel)
+env_model_config = ConfigDict(
+    strict=True,
+    frozen=True,
+    extra="forbid",
+)
+
+
+class BaseEnv(BaseModel):
+    model_config = env_model_config
+
+
+EnvSchema = TypeVar("EnvSchema", bound=BaseEnv)
 
 
 def get_validated_env(
diff --git a/shared/pyproject.toml b/shared/pyproject.toml
index 3fb450fb..c17f3dc7 100644
--- a/shared/pyproject.toml
+++ b/shared/pyproject.toml
@@ -5,6 +5,7 @@ description = "Shared utilities for the Exo project"
 readme = "README.md"
 requires-python = ">=3.13"
 dependencies = [
+    "pathlib>=1.0.1",
     "protobuf>=6.31.1",
     "pydantic>=2.11.7",
     "rich>=14.0.0",
@@ -30,4 +31,4 @@ exclude = ["protobufs/schemas", "*.md", "pyproject.toml"]
 [dependency-groups]
 dev = [
     "types-protobuf>=6.30.2.20250516",
-]
\ No newline at end of file
+]
diff --git a/shared/types/event_sourcing.py b/shared/types/event_sourcing.py
new file mode 100644
index 00000000..33fc89e6
--- /dev/null
+++ b/shared/types/event_sourcing.py
@@ -0,0 +1,99 @@
+from typing import (
+    Annotated,
+    Callable,
+    Generic,
+    Literal,
+    Protocol,
+    Sequence,
+    Tuple,
+    TypeVar,
+    get_args,
+)
+from uuid import UUID
+
+from pydantic import BaseModel, Field, TypeAdapter
+from pydantic.types import UuidVersion
+
+_EventId = Annotated[UUID, UuidVersion(4)]
+EventId = type("EventID", (UUID,), {})
+EventIdParser: TypeAdapter[EventId] = TypeAdapter(_EventId)
+
+EventTypes = Literal["create", "update", "delete"]
+EventTypeT = TypeVar("EventTypeT", bound=EventTypes)
+TEventType = TypeVar("TEventType", bound=EventTypes, covariant=True)
+
+
+class Event(BaseModel, Generic[TEventType]):
+    event_type: TEventType
+    idem_key: EventId
+
+
+class State(BaseModel, Generic[EventTypeT]):
+    event_types: tuple[EventTypeT, ...] = get_args(EventTypeT)
+    sequence_number: int = Field(default=0, ge=0)
+
+
+AnnotatedEventType = Annotated[EventTypes, Field(discriminator="event_type")]
+EventTypeParser: TypeAdapter[AnnotatedEventType] = TypeAdapter(AnnotatedEventType)
+
+Applicator = Callable[[State[EventTypeT], Event[TEventType]], State[EventTypeT]]
+Apply = Callable[[State[EventTypeT], Event[EventTypeT]], State[EventTypeT]]
+SagaApplicator = Callable[
+    [State[EventTypeT], Event[TEventType]], Sequence[Event[EventTypeT]]
+]
+Saga = Callable[[State[EventTypeT], Event[EventTypeT]], Sequence[Event[EventTypeT]]]
+
+StateAndEvent = Tuple[State[EventTypeT], Event[EventTypeT]]
+EffectHandler = Callable[[StateAndEvent[EventTypeT], State[EventTypeT]], None]
+EventPublisher = Callable[[Event[EventTypeT]], None]
+
+
+class EventOutbox(Protocol):
+    def send(self, events: Sequence[Event[EventTypeT]]) -> None: ...
+
+
+class EventProcessor(Protocol):
+    def update(
+        self,
+        state: State[EventTypeT],
+        apply: Apply[EventTypeT],
+        effect_handlers: Sequence[EffectHandler[EventTypeT]],
+    ) -> State[EventTypeT]: ...
+
+
+def get_saga_effect_handler(
+    sagas: Saga[EventTypeT], event_publisher: EventPublisher[EventTypeT]
+) -> EffectHandler[EventTypeT]:
+    def effect_handler(state_and_event: StateAndEvent[EventTypeT]) -> None:
+        trigger_state, trigger_event = state_and_event
+        for event in sagas(trigger_state, trigger_event):
+            event_publisher(event)
+
+    return lambda state_and_event, _: effect_handler(state_and_event)
+
+
+def get_effects_from_sagas(
+    sagas: Sequence[Saga[EventTypeT]], event_publisher: EventPublisher[EventTypeT]
+) -> Sequence[EffectHandler[EventTypeT]]:
+    return [get_saga_effect_handler(saga, event_publisher) for saga in sagas]
+
+
+IdemKeyGenerator = Callable[[State[EventTypeT], int], Sequence[EventId]]
+
+_CommandId = Annotated[UUID, UuidVersion(4)]
+CommandId = type("CommandID", (UUID,), {})
+CommandIdParser: TypeAdapter[CommandId] = TypeAdapter(_CommandId)
+
+CommandTypes = Literal["create", "update", "delete"]
+CommandTypeT = TypeVar("CommandTypeT", bound=EventTypes)
+TCommandType = TypeVar("TCommandType", bound=EventTypes, covariant=True)
+
+
+class Command(BaseModel, Generic[TEventType, TCommandType]):
+    command_type: TCommandType
+    idem_key: CommandId
+
+
+Decide = Callable[
+    [State[EventTypeT], Command[TEventType, TCommandType]], Sequence[Event[EventTypeT]]
+]
diff --git a/uv.lock b/uv.lock
index d0b632cf..825473ce 100644
--- a/uv.lock
+++ b/uv.lock
@@ -15,6 +15,7 @@ members = [
     "exo",
     "exo-engine-mlx",
     "exo-master",
+    "exo-networking",
     "exo-shared",
     "exo-worker",
 ]
@@ -57,6 +58,7 @@ darwin = [
 [package.dev-dependencies]
 dev = [
     { name = "basedpyright", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
+    { name = "maturin", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
     { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
     { name = "ruff", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 ]
@@ -72,6 +74,7 @@ provides-extras = ["darwin"]
 [package.metadata.requires-dev]
 dev = [
     { name = "basedpyright", specifier = ">=1.29.4" },
+    { name = "maturin", specifier = ">=1.9.0" },
     { name = "pytest", specifier = ">=8.4.0" },
     { name = "ruff", specifier = ">=0.11.13" },
 ]
@@ -92,11 +95,17 @@ dependencies = [
 [package.metadata]
 requires-dist = [{ name = "exo-shared", editable = "shared" }]
 
+[[package]]
+name = "exo-networking"
+version = "0.1.0"
+source = { editable = "networking" }
+
 [[package]]
 name = "exo-shared"
 version = "0.1.0"
 source = { editable = "shared" }
 dependencies = [
+    { name = "pathlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
     { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
     { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
     { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -109,6 +118,7 @@ dev = [
 
 [package.metadata]
 requires-dist = [
+    { name = "pathlib", specifier = ">=1.0.1" },
     { name = "protobuf", specifier = ">=6.31.1" },
     { name = "pydantic", specifier = ">=2.11.7" },
     { name = "rich", specifier = ">=14.0.0" },
@@ -149,6 +159,23 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
 ]
 
+[[package]]
+name = "maturin"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/3a/117a238e055c7d9de5a27619e09f2762830f3ea227f69e110d86e2ec5bd9/maturin-1.9.0.tar.gz", hash = "sha256:ccb9cb87f8df88d1bab8f49efe3fc77f0abb0639ea4b4ebf4f35549200d16b9e", size = 209543, upload-time = "2025-06-23T14:36:05.768Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b7/3f/3063ce9ace8fe33e02cc05209551a5a0d0af9b7990b14e063876ff149e82/maturin-1.9.0-py3-none-linux_armv6l.whl", hash = "sha256:18d77e395f62a0227697098526be6becb3ceea34a79f338b1b716fb96e42a1b2", size = 8130784, upload-time = "2025-06-23T14:35:35.813Z" },
+    { url = "https://files.pythonhosted.org/packages/97/52/cb5491ad290002186af3bcb4768f7bb5c6c8d6917cf0a98b945533cd8c04/maturin-1.9.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:33f046f52327b68c28203efe5ecc4fd1952b4d1fe34e65853092e3347a6a6fa0", size = 16082407, upload-time = "2025-06-23T14:35:39.584Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/9c/c6fd50c23875fc741651b2fedfffdf4f671cb74c46e66f365d1f9b861daf/maturin-1.9.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6b075f82dc87fa70d583b1fe909ac5e96f36ec2043721acb82f9d6757e860459", size = 8405709, upload-time = "2025-06-23T14:35:42.248Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/44/bf61ff9d3f0db8c5a868da55e7827e5fb1a82642705384bcc85bc9a1918f/maturin-1.9.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:c99003470cb37388a31152af4b00492c5db8d767f689a64f45eb5830adc6f3f4", size = 8152167, upload-time = "2025-06-23T14:35:45.013Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/99/634aa686a41f899b39300c28ecca756974609e65e80e7a1b7a77765bd070/maturin-1.9.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:35a506c3139d6847edd160f99fd0da7c7b2bbb4d53e0fef995479eed3a92ac37", size = 8808959, upload-time = "2025-06-23T14:35:47.099Z" },
+    { url = "https://files.pythonhosted.org/packages/98/4d/4cfa79bad83d2722c47c058f0b527ac5f27c852845b9e79aca95e4fe09c5/maturin-1.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a48d8917e60875a06ef36568c2c4a926b6e2681616a251cc50cbf0a5c8aa7428", size = 7911691, upload-time = "2025-06-23T14:35:49.768Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/8b/a9410f5ebccad93f86539ab2f77a7aabb9dd05396f9238125c946dc0798c/maturin-1.9.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:5a7a829b03415b7fcaaabeafb520a92cd32b6dd9e8d12e34c7cd7689d404e6a3", size = 7990238, upload-time = "2025-06-23T14:35:51.8Z" },
+    { url = "https://files.pythonhosted.org/packages/13/8c/9dd88d5a30717a01793f81ad561b4e77316e0e6154f73e8b072b9ad3378e/maturin-1.9.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:3aa8de021f91bd41918f4afd1b285e84e1b858e354b1de01597bb97a1b9820e1", size = 10134367, upload-time = "2025-06-23T14:35:54.288Z" },
+    { url = "https://files.pythonhosted.org/packages/35/34/bb85f46570b4ff2e7bf0dfb8c7408855df811f15d0c1a22896a4699ac0ac/maturin-1.9.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:289d0c2925a8c8ba3ce058e7b691b1c274fd06e36a915232f4e07fa62266f9b6", size = 9001993, upload-time = "2025-06-23T14:35:56.692Z" },
+]
+
 [[package]]
 name = "mdurl"
 version = "0.1.2"
@@ -192,6 +219,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
 ]
 
+[[package]]
+name = "pathlib"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ac/aa/9b065a76b9af472437a0059f77e8f962fe350438b927cb80184c32f075eb/pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f", size = 49298, upload-time = "2014-09-03T15:41:57.18Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/78/f9/690a8600b93c332de3ab4a344a4ac34f00c8f104917061f779db6a918ed6/pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147", size = 14363, upload-time = "2022-05-04T13:37:20.585Z" },
+]
+
 [[package]]
 name = "pluggy"
 version = "1.6.0"

← d8459358 Refactor CI  ·  back to Exo  ·  refactor: Add all event types with Event implementations 5ba230ed →