pykardia 0.1.0.dev1__py3-none-any.whl

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 (91) hide show
  1. pykardia/__init__.py +139 -0
  2. pykardia/__main__.py +5 -0
  3. pykardia/catalog.py +17 -0
  4. pykardia/cli.py +103 -0
  5. pykardia/core/__init__.py +103 -0
  6. pykardia/core/cleanup.py +128 -0
  7. pykardia/core/config.py +213 -0
  8. pykardia/core/context.py +619 -0
  9. pykardia/core/deps.py +53 -0
  10. pykardia/core/effect.py +209 -0
  11. pykardia/core/error.py +149 -0
  12. pykardia/core/events.py +145 -0
  13. pykardia/core/fiber.py +243 -0
  14. pykardia/core/id_table.py +55 -0
  15. pykardia/core/json.py +295 -0
  16. pykardia/core/key.py +88 -0
  17. pykardia/core/logger.py +186 -0
  18. pykardia/core/plugin.py +262 -0
  19. pykardia/core/registry.py +107 -0
  20. pykardia/core/root.py +149 -0
  21. pykardia/core/runtime.py +146 -0
  22. pykardia/core/scope.py +190 -0
  23. pykardia/core/service.py +123 -0
  24. pykardia/core/state.py +555 -0
  25. pykardia/core/status.py +14 -0
  26. pykardia/core/value.py +51 -0
  27. pykardia/core/wire.py +179 -0
  28. pykardia/decorators.py +144 -0
  29. pykardia/fs/__init__.py +18 -0
  30. pykardia/fs/filter.py +92 -0
  31. pykardia/fs/glob.py +121 -0
  32. pykardia/fs/native.py +63 -0
  33. pykardia/fs/service.py +55 -0
  34. pykardia/fs/watcher.py +216 -0
  35. pykardia/guest/__init__.py +18 -0
  36. pykardia/guest/abi.py +75 -0
  37. pykardia/guest/api.py +90 -0
  38. pykardia/guest/export.py +225 -0
  39. pykardia/guest/meta.py +68 -0
  40. pykardia/guest/native.py +122 -0
  41. pykardia/guest/process.py +91 -0
  42. pykardia/guest/runtime.py +70 -0
  43. pykardia/guest/templates/wasm.c +124 -0
  44. pykardia/guest/wasm_build.py +252 -0
  45. pykardia/hmr/__init__.py +37 -0
  46. pykardia/hmr/dylib.py +185 -0
  47. pykardia/hmr/graph.py +212 -0
  48. pykardia/hmr/guest.py +292 -0
  49. pykardia/hmr/hmr.py +124 -0
  50. pykardia/hmr/process.py +170 -0
  51. pykardia/hmr/transaction.py +287 -0
  52. pykardia/hmr/wasm.py +173 -0
  53. pykardia/hmr/watch.py +88 -0
  54. pykardia/include/__init__.py +27 -0
  55. pykardia/include/document.py +143 -0
  56. pykardia/include/identity.py +76 -0
  57. pykardia/include/include.py +614 -0
  58. pykardia/include/journal.py +204 -0
  59. pykardia/include/patch.py +297 -0
  60. pykardia/include/tree.py +105 -0
  61. pykardia/include/yaml.py +432 -0
  62. pykardia/loader/__init__.py +63 -0
  63. pykardia/loader/_unicode.py +3242 -0
  64. pykardia/loader/backend.py +95 -0
  65. pykardia/loader/catalog.py +83 -0
  66. pykardia/loader/expr.py +562 -0
  67. pykardia/loader/group.py +25 -0
  68. pykardia/loader/handles.py +288 -0
  69. pykardia/loader/loader.py +915 -0
  70. pykardia/loader/options.py +195 -0
  71. pykardia/loader/state.py +333 -0
  72. pykardia/logger_console/__init__.py +19 -0
  73. pykardia/logger_console/config.py +38 -0
  74. pykardia/logger_console/exporter.py +85 -0
  75. pykardia/logger_console/format.py +139 -0
  76. pykardia/logger_console/plugin.py +18 -0
  77. pykardia/prelude.py +23 -0
  78. pykardia/py.typed +0 -0
  79. pykardia/timer/__init__.py +16 -0
  80. pykardia/timer/context.py +13 -0
  81. pykardia/timer/schedule.py +85 -0
  82. pykardia/timer/service.py +221 -0
  83. pykardia/timer/ticks.py +96 -0
  84. pykardia/version.py +3 -0
  85. pykardia-0.1.0.dev1.dist-info/METADATA +70 -0
  86. pykardia-0.1.0.dev1.dist-info/RECORD +91 -0
  87. pykardia-0.1.0.dev1.dist-info/WHEEL +5 -0
  88. pykardia-0.1.0.dev1.dist-info/entry_points.txt +2 -0
  89. pykardia-0.1.0.dev1.dist-info/licenses/LICENSE +674 -0
  90. pykardia-0.1.0.dev1.dist-info/licenses/NOTICE +50 -0
  91. pykardia-0.1.0.dev1.dist-info/top_level.txt +1 -0
pykardia/__init__.py ADDED
@@ -0,0 +1,139 @@
1
+ """PyKardia: revertible effects and reactive services."""
2
+
3
+ from pykardia import fs, guest, hmr, include, loader, logger_console, prelude, timer
4
+ from pykardia.catalog import catalog
5
+ from pykardia.core import (
6
+ AccessError,
7
+ AggregateError,
8
+ AlreadyProvided,
9
+ Check,
10
+ ConfigCommitted,
11
+ Context,
12
+ Deps,
13
+ Disposal,
14
+ Disposer,
15
+ EffectHandle,
16
+ EffectMeta,
17
+ EffectScope,
18
+ ErasedConfig,
19
+ Error,
20
+ Event,
21
+ Exporter,
22
+ Fiber,
23
+ FiberId,
24
+ FiberState,
25
+ ForeignBinding,
26
+ IdTable,
27
+ ImplId,
28
+ Inactive,
29
+ InactiveEffect,
30
+ Inject,
31
+ Injected,
32
+ Interrupted,
33
+ Issue,
34
+ JsonValue,
35
+ Key,
36
+ Level,
37
+ Logger,
38
+ Message,
39
+ NoConfig,
40
+ NotProvided,
41
+ Overlay,
42
+ Panicked,
43
+ PathSegment,
44
+ Plugin,
45
+ PluginDef,
46
+ PluginInserted,
47
+ PluginRemoved,
48
+ PyKardiaError,
49
+ RealmId,
50
+ Registry,
51
+ Replaced,
52
+ RuntimeKey,
53
+ Service,
54
+ ServiceChanged,
55
+ Settle,
56
+ StatusChanged,
57
+ Svc,
58
+ TypeMismatch,
59
+ Undeclared,
60
+ ValidationError,
61
+ decode,
62
+ hyphenate,
63
+ merge_shallow,
64
+ )
65
+ from pykardia.decorators import plugin, service
66
+ from pykardia.version import __version__
67
+
68
+ __all__: list[str] = [
69
+ "AccessError",
70
+ "AggregateError",
71
+ "AlreadyProvided",
72
+ "Check",
73
+ "ConfigCommitted",
74
+ "Context",
75
+ "Deps",
76
+ "Disposal",
77
+ "Disposer",
78
+ "EffectHandle",
79
+ "EffectMeta",
80
+ "EffectScope",
81
+ "ErasedConfig",
82
+ "Error",
83
+ "Event",
84
+ "Exporter",
85
+ "Fiber",
86
+ "FiberId",
87
+ "FiberState",
88
+ "ForeignBinding",
89
+ "IdTable",
90
+ "ImplId",
91
+ "Inactive",
92
+ "InactiveEffect",
93
+ "Inject",
94
+ "Injected",
95
+ "Interrupted",
96
+ "Issue",
97
+ "JsonValue",
98
+ "PyKardiaError",
99
+ "Key",
100
+ "Level",
101
+ "Logger",
102
+ "Message",
103
+ "NoConfig",
104
+ "NotProvided",
105
+ "Overlay",
106
+ "Panicked",
107
+ "PathSegment",
108
+ "Plugin",
109
+ "PluginDef",
110
+ "PluginInserted",
111
+ "PluginRemoved",
112
+ "RealmId",
113
+ "Registry",
114
+ "Replaced",
115
+ "RuntimeKey",
116
+ "Service",
117
+ "ServiceChanged",
118
+ "Settle",
119
+ "StatusChanged",
120
+ "Svc",
121
+ "TypeMismatch",
122
+ "Undeclared",
123
+ "ValidationError",
124
+ "__version__",
125
+ "catalog",
126
+ "decode",
127
+ "fs",
128
+ "guest",
129
+ "hmr",
130
+ "hyphenate",
131
+ "include",
132
+ "loader",
133
+ "logger_console",
134
+ "merge_shallow",
135
+ "plugin",
136
+ "prelude",
137
+ "service",
138
+ "timer",
139
+ ]
pykardia/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Module execution entry for the pykardia command-line application."""
2
+
3
+ from pykardia.cli import main
4
+
5
+ raise SystemExit(main())
pykardia/catalog.py ADDED
@@ -0,0 +1,17 @@
1
+ """Unified catalog for built-in plugins and their original Cordis package aliases."""
2
+
3
+ from pykardia.fs import FsPlugin
4
+ from pykardia.include import IncludePlugin
5
+ from pykardia.loader import Catalog, builtins
6
+ from pykardia.logger_console import ConsolePlugin
7
+ from pykardia.timer import TimerPlugin
8
+
9
+
10
+ def catalog() -> Catalog:
11
+ """Create a fresh catalog with all built-ins and no implicit HMR module graph."""
12
+ result = builtins()
13
+ result.register(TimerPlugin).alias("@cordisjs/plugin-timer", "timer")
14
+ result.register(ConsolePlugin).alias("@cordisjs/plugin-logger-console", "logger-console")
15
+ result.register(FsPlugin).alias("@cordisjs/plugin-fs-node", "fs")
16
+ result.register(IncludePlugin).alias("@cordisjs/plugin-include", "include")
17
+ return result
pykardia/cli.py ADDED
@@ -0,0 +1,103 @@
1
+ """Run a live file-backed pykardia application until Ctrl-C."""
2
+
3
+ import asyncio
4
+ import signal
5
+ import sys
6
+ from collections.abc import Awaitable
7
+ from pathlib import Path
8
+ from types import FrameType
9
+
10
+ from pykardia.catalog import catalog
11
+ from pykardia.core.context import Context
12
+ from pykardia.core.plugin import PluginDef
13
+ from pykardia.fs import FsPlugin
14
+ from pykardia.hmr import DylibSource, Hmr, ModuleCode, ModuleGraph, ModuleSource, WasmSource
15
+ from pykardia.loader import Catalog, Loader, Resolver
16
+ from pykardia.logger_console import ConsoleConfig, ConsoleExporter
17
+
18
+
19
+ def main() -> int:
20
+ """Start with the first path argument, defaulting to pykardia.yml."""
21
+ return asyncio.run(start(sys.argv[1] if len(sys.argv) > 1 else "pykardia.yml"))
22
+
23
+
24
+ async def start(path: str) -> int:
25
+ """Report startup or cleanup errors through the app logger and return its exit status."""
26
+ root = Context()
27
+ status = 0
28
+ try:
29
+ await run(root, path)
30
+ except Exception as error:
31
+ root.logger_named("app").log_error(error)
32
+ status = 1
33
+ try:
34
+ await root.fiber().dispose()
35
+ except Exception as error:
36
+ root.logger_named("app").log_error(error)
37
+ status = 1
38
+ return status
39
+
40
+
41
+ async def run(root: Context, path: str) -> None:
42
+ """Boot console, fs, loader and watched include in source order, then await Ctrl-C."""
43
+ root.add_exporter(ConsoleExporter(ConsoleConfig()))
44
+ root.plugin(FsPlugin, None)
45
+ plugins = catalog()
46
+ graph = ModuleGraph(GuestFiles())
47
+ plugins.register_def("hmr", Hmr.plugin(graph)).alias("@cordisjs/plugin-hmr", "hmr")
48
+ resolver = Plugins(plugins, graph, Path(path).absolute().parent)
49
+ await root.plugin_def(Loader.plugin(resolver)).settled()
50
+ loader = root.get(Loader).arc()
51
+ loader.set_logs(True)
52
+ id = await loader.create({"name": "include", "config": {"path": path, "watch": True}})
53
+ fiber = loader.resolve(id).inner_fiber()
54
+ if fiber is not None and (error := fiber.error()) is not None:
55
+ raise error
56
+ await interrupted()
57
+
58
+
59
+ async def interrupted() -> None:
60
+ """Wait for Ctrl-C or Windows Ctrl-Break and restore the previous signal handlers."""
61
+ loop = asyncio.get_running_loop()
62
+ stopped = asyncio.Event()
63
+ signals = (signal.SIGINT, signal.SIGBREAK) if sys.platform == "win32" else (signal.SIGINT,)
64
+ previous = {number: signal.getsignal(number) for number in signals}
65
+
66
+ def stop(number: int, frame: FrameType | None) -> None:
67
+ """Wake the event loop even when it is waiting on Windows I/O."""
68
+ loop.call_soon_threadsafe(stopped.set)
69
+
70
+ for number in signals:
71
+ signal.signal(number, stop)
72
+ try:
73
+ await stopped.wait()
74
+ finally:
75
+ for number, handler in previous.items():
76
+ signal.signal(number, handler)
77
+
78
+
79
+ class Plugins(Resolver):
80
+ """Resolve built-in names before loading guest paths relative to the root config file."""
81
+
82
+ def __init__(self, plugins: Catalog, graph: ModuleGraph, base: Path) -> None:
83
+ """Retain a shared catalog, module graph and lexical base directory."""
84
+ self.catalog, self.graph, self.base = plugins, graph, base
85
+
86
+ def resolve(self, name: str) -> Awaitable[PluginDef]:
87
+ """Snapshot built-ins or import the corresponding guest module."""
88
+ if self.catalog.get(name) is not None:
89
+ return self.catalog.resolve(name)
90
+ return self.graph.resolve(str(self.base / name))
91
+
92
+
93
+ class GuestFiles(ModuleSource):
94
+ """Select WASM for .wasm/.wat and the native C ABI for other file extensions."""
95
+
96
+ def __init__(self) -> None:
97
+ """Retain one shared WASM engine and a native loader."""
98
+ self.wasm, self.dylib = WasmSource(), DylibSource()
99
+
100
+ def load(self, id: str) -> ModuleCode:
101
+ """Load the selected real backend without treating executables as CLI process guests."""
102
+ source = self.wasm if Path(id).suffix in (".wasm", ".wat") else self.dylib
103
+ return source.load(id)
@@ -0,0 +1,103 @@
1
+ """Core runtime entry points."""
2
+
3
+ from pykardia.core.cleanup import Disposal, Disposer
4
+ from pykardia.core.config import ErasedConfig, Issue, NoConfig, PathSegment, ValidationError, decode
5
+ from pykardia.core.context import Context, merge_shallow
6
+ from pykardia.core.deps import Deps, Injected
7
+ from pykardia.core.effect import EffectHandle, EffectMeta, EffectScope
8
+ from pykardia.core.error import (
9
+ AccessError,
10
+ AggregateError,
11
+ AlreadyProvided,
12
+ Error,
13
+ ForeignBinding,
14
+ Inactive,
15
+ InactiveEffect,
16
+ Interrupted,
17
+ NotProvided,
18
+ Panicked,
19
+ PyKardiaError,
20
+ TypeMismatch,
21
+ Undeclared,
22
+ )
23
+ from pykardia.core.events import (
24
+ ConfigCommitted,
25
+ Event,
26
+ PluginInserted,
27
+ PluginRemoved,
28
+ Replaced,
29
+ ServiceChanged,
30
+ StatusChanged,
31
+ )
32
+ from pykardia.core.fiber import Fiber, Settle
33
+ from pykardia.core.id_table import IdTable
34
+ from pykardia.core.key import FiberId, ImplId, Key, RealmId
35
+ from pykardia.core.logger import Exporter, Level, Logger, Message, hyphenate
36
+ from pykardia.core.plugin import Inject, Plugin, PluginDef, RuntimeKey
37
+ from pykardia.core.registry import Registry
38
+ from pykardia.core.scope import Overlay
39
+ from pykardia.core.service import Service, Svc
40
+ from pykardia.core.state import Check
41
+ from pykardia.core.status import FiberState
42
+ from pykardia.core.value import JsonValue
43
+
44
+ __all__: list[str] = [
45
+ "AccessError",
46
+ "AggregateError",
47
+ "AlreadyProvided",
48
+ "Check",
49
+ "ConfigCommitted",
50
+ "Context",
51
+ "Deps",
52
+ "Disposal",
53
+ "Disposer",
54
+ "EffectHandle",
55
+ "EffectMeta",
56
+ "EffectScope",
57
+ "ErasedConfig",
58
+ "Error",
59
+ "Event",
60
+ "Exporter",
61
+ "Fiber",
62
+ "FiberId",
63
+ "FiberState",
64
+ "ForeignBinding",
65
+ "IdTable",
66
+ "ImplId",
67
+ "Inactive",
68
+ "InactiveEffect",
69
+ "Inject",
70
+ "Injected",
71
+ "Interrupted",
72
+ "Issue",
73
+ "JsonValue",
74
+ "PyKardiaError",
75
+ "Key",
76
+ "Level",
77
+ "Logger",
78
+ "Message",
79
+ "NoConfig",
80
+ "NotProvided",
81
+ "Overlay",
82
+ "Panicked",
83
+ "PathSegment",
84
+ "Plugin",
85
+ "PluginDef",
86
+ "PluginInserted",
87
+ "PluginRemoved",
88
+ "RealmId",
89
+ "Registry",
90
+ "Replaced",
91
+ "RuntimeKey",
92
+ "Service",
93
+ "ServiceChanged",
94
+ "Settle",
95
+ "StatusChanged",
96
+ "Svc",
97
+ "TypeMismatch",
98
+ "Undeclared",
99
+ "ValidationError",
100
+ "decode",
101
+ "hyphenate",
102
+ "merge_shallow",
103
+ ]
@@ -0,0 +1,128 @@
1
+ """Reversible actions with a synchronous prefix and sequential LIFO cleanup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Awaitable, Callable, Generator
7
+ from typing import Protocol
8
+
9
+ from pykardia.core.runtime import Completion, Runtime
10
+
11
+
12
+ class Disposal:
13
+ """Observe cleanup that has already started and proceeds without its observer."""
14
+
15
+ def __init__(self, completion: Completion[None]) -> None:
16
+ """Wrap the runtime-owned result of a cleanup sequence."""
17
+ self._completion = completion
18
+ self._consumed = False
19
+
20
+ def is_ready(self) -> bool:
21
+ """Return whether cleanup finished synchronously, even after async completion."""
22
+ return self._completion.is_ready()
23
+
24
+ def _take_ready(self) -> None:
25
+ """Consume a synchronous result, matching Disposal::Ready's one-shot result."""
26
+ if not self._consumed:
27
+ self._consumed = True
28
+ self._completion.result()
29
+
30
+ async def wait(self) -> None:
31
+ """Observe cleanup without propagating waiter cancellation into its task."""
32
+ if self.is_ready():
33
+ self._take_ready()
34
+ else:
35
+ await self._completion
36
+
37
+ def __await__(self) -> Generator[object, None, None]:
38
+ """Wait for the cleanup's result."""
39
+ return self.wait().__await__()
40
+
41
+
42
+ class Disposable(Protocol):
43
+ """An effect handle whose inverse can join another cleanup sequence."""
44
+
45
+ def dispose(self) -> Disposal:
46
+ """Start cleanup and return its completion."""
47
+ ...
48
+
49
+
50
+ class Disposer:
51
+ """An inverse consumed at most once; its callback runs when disposal starts."""
52
+
53
+ def __init__(self, action: Callable[[], Awaitable[None] | None]) -> None:
54
+ """Retain a callback that may finish immediately or return asynchronous work."""
55
+ self._action: Callable[[], Awaitable[None] | None] | None = action
56
+
57
+ @classmethod
58
+ def new(cls, action: Callable[[], None]) -> Disposer:
59
+ """Create a synchronous inverse; exceptions become cleanup failures."""
60
+ return cls(action)
61
+
62
+ fallible = new
63
+
64
+ @classmethod
65
+ def future(cls, action: Callable[[], Awaitable[None]]) -> Disposer:
66
+ """Create an asynchronous inverse with a synchronously invoked factory."""
67
+ return cls(action)
68
+
69
+ try_future = future
70
+
71
+ @classmethod
72
+ def from_handle(cls, handle: Disposable) -> Disposer:
73
+ """Adopt a nested handle without turning synchronous cleanup asynchronous."""
74
+ return cls(handle.dispose)
75
+
76
+ def call(self) -> Awaitable[None] | None:
77
+ """Consume the inverse and return only its unfinished asynchronous portion."""
78
+ action, self._action = self._action, None
79
+ if action is None:
80
+ return None
81
+ result = action()
82
+ if isinstance(result, Disposal) and result.is_ready():
83
+ result._take_ready()
84
+ return None
85
+ return result
86
+
87
+
88
+ def run_sequence(runtime: Runtime, disposers: list[Disposer]) -> Disposal:
89
+ """Run inverses newest first, continuing after failures and reporting the first.
90
+
91
+ Synchronous callbacks run before this function returns. From the first awaitable
92
+ onward, the runtime owns a task which runs the remainder strictly in sequence.
93
+ """
94
+ rest = list(disposers)
95
+ first_error: BaseException | None = None
96
+ while rest:
97
+ try:
98
+ pending = rest.pop().call()
99
+ except (Exception, asyncio.CancelledError) as error:
100
+ if first_error is None:
101
+ first_error = error
102
+ continue
103
+ if pending is not None:
104
+ return Disposal(runtime.spawn(_finish_sequence(pending, rest, first_error)))
105
+ return Disposal(
106
+ Completion.ready(None) if first_error is None else Completion.failed(first_error)
107
+ )
108
+
109
+
110
+ async def _finish_sequence(
111
+ pending: Awaitable[None], rest: list[Disposer], first_error: BaseException | None
112
+ ) -> None:
113
+ """Finish the first pending inverse, then run all remaining inverses in order."""
114
+ try:
115
+ await pending
116
+ except (Exception, asyncio.CancelledError) as error:
117
+ if first_error is None:
118
+ first_error = error
119
+ while rest:
120
+ try:
121
+ following = rest.pop().call()
122
+ if following is not None:
123
+ await following
124
+ except (Exception, asyncio.CancelledError) as error:
125
+ if first_error is None:
126
+ first_error = error
127
+ if first_error is not None:
128
+ raise first_error