engin 0.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.
engin/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ from engin._assembler import Assembler
2
+ from engin._block import Block, invoke, provide
3
+ from engin._dependency import Entrypoint, Invoke, Provide, Supply
4
+ from engin._engin import Engin, Option
5
+ from engin._exceptions import AssemblyError
6
+ from engin._lifecycle import Lifecycle
7
+
8
+ __all__ = [
9
+ "Assembler",
10
+ "AssemblyError",
11
+ "Engin",
12
+ "Entrypoint",
13
+ "Invoke",
14
+ "Lifecycle",
15
+ "Block",
16
+ "Option",
17
+ "Provide",
18
+ "Supply",
19
+ "invoke",
20
+ "provide",
21
+ ]
engin/_assembler.py ADDED
@@ -0,0 +1,126 @@
1
+ import asyncio
2
+ import logging
3
+ from collections import defaultdict
4
+ from collections.abc import Collection, Iterable
5
+ from dataclasses import dataclass
6
+ from inspect import BoundArguments, Signature
7
+ from typing import Any, Generic, TypeVar
8
+
9
+ from engin._dependency import Dependency, Provide, Supply
10
+ from engin._exceptions import AssemblyError
11
+ from engin._type_utils import TypeId, type_id_of
12
+
13
+ LOG = logging.getLogger("engin")
14
+
15
+ T = TypeVar("T")
16
+
17
+
18
+ @dataclass(slots=True, kw_only=True, frozen=True)
19
+ class AssembledDependency(Generic[T]):
20
+ dependency: Dependency[Any, T]
21
+ bound_args: BoundArguments
22
+
23
+ async def __call__(self) -> T:
24
+ return await self.dependency(*self.bound_args.args, **self.bound_args.kwargs)
25
+
26
+
27
+ class Assembler:
28
+ def __init__(self, providers: Iterable[Provide]) -> None:
29
+ self._providers: dict[TypeId, Provide[Any]] = {}
30
+ self._multiproviders: dict[TypeId, list[Provide[list[Any]]]] = defaultdict(list)
31
+ self._dependencies: dict[TypeId, Any] = {}
32
+ self._consumed_providers: set[Provide[Any]] = set()
33
+ self._lock = asyncio.Lock()
34
+
35
+ for provider in providers:
36
+ type_id = provider.return_type_id
37
+ if not provider.is_multiprovider:
38
+ if type_id in self._providers:
39
+ raise RuntimeError(f"A Provider already exists for '{type_id}'")
40
+ self._providers[type_id] = provider
41
+ else:
42
+ self._multiproviders[type_id].append(provider)
43
+
44
+ def _resolve_providers(self, type_id: TypeId) -> Collection[Provide]:
45
+ if type_id.multi:
46
+ providers = self._multiproviders.get(type_id)
47
+ else:
48
+ providers = [provider] if (provider := self._providers.get(type_id)) else None
49
+ if not providers:
50
+ if type_id.multi:
51
+ LOG.warning(f"no provider for '{type_id}' defaulting to empty list")
52
+ providers = [(Supply([], type_hint=list[type_id.type]))]
53
+ else:
54
+ raise LookupError(f"No Provider registered for dependency '{type_id}'")
55
+
56
+ required_providers: list[Provide[Any]] = []
57
+ for provider in providers:
58
+ required_providers.extend(
59
+ provider
60
+ for provider_param in provider.parameter_types
61
+ for provider in self._resolve_providers(provider_param)
62
+ )
63
+
64
+ return {*required_providers, *providers}
65
+
66
+ async def _satisfy(self, target: TypeId) -> None:
67
+ for provider in self._resolve_providers(target):
68
+ if provider in self._consumed_providers:
69
+ continue
70
+ self._consumed_providers.add(provider)
71
+ type_id = provider.return_type_id
72
+ bound_args = await self._bind_arguments(provider.signature)
73
+ try:
74
+ value = await provider(*bound_args.args, **bound_args.kwargs)
75
+ except Exception as err:
76
+ raise AssemblyError(
77
+ provider=provider, error_type=type(err), error_message=str(err)
78
+ ) from err
79
+ if provider.is_multiprovider:
80
+ if type_id in self._dependencies:
81
+ self._dependencies[type_id].extend(value)
82
+ else:
83
+ self._dependencies[type_id] = value
84
+ else:
85
+ self._dependencies[type_id] = value
86
+
87
+ async def _bind_arguments(self, signature: Signature) -> BoundArguments:
88
+ args = []
89
+ kwargs = {}
90
+ for param_name, param in signature.parameters.items():
91
+ if param_name == "self":
92
+ args.append(object())
93
+ continue
94
+ param_key = type_id_of(param.annotation)
95
+ has_dependency = param_key in self._dependencies
96
+ if not has_dependency:
97
+ await self._satisfy(param_key)
98
+ val = self._dependencies[param_key]
99
+ if param.kind == param.POSITIONAL_ONLY:
100
+ args.append(val)
101
+ else:
102
+ kwargs[param.name] = val
103
+
104
+ return signature.bind(*args, **kwargs)
105
+
106
+ async def assemble(self, dependency: Dependency[Any, T]) -> AssembledDependency[T]:
107
+ async with self._lock:
108
+ return AssembledDependency(
109
+ dependency=dependency,
110
+ bound_args=await self._bind_arguments(dependency.signature),
111
+ )
112
+
113
+ async def get(self, type_: type[T]) -> T:
114
+ type_id = type_id_of(type_)
115
+ if type_id.multi:
116
+ out = []
117
+ for provider in self._multiproviders[type_id]:
118
+ assembled_dependency = await self.assemble(provider)
119
+ out.extend(await assembled_dependency())
120
+ return out
121
+ else:
122
+ assembled_dependency = await self.assemble(self._providers[type_id])
123
+ return await assembled_dependency()
124
+
125
+ def has(self, type_: type[T]) -> bool:
126
+ return type_id_of(type_) in self._providers
engin/_block.py ADDED
@@ -0,0 +1,36 @@
1
+ import inspect
2
+ from collections.abc import Iterable, Iterator
3
+
4
+ from engin._dependency import Func, Invoke, Provide
5
+
6
+
7
+ def provide(func: Func) -> Func:
8
+ func._opt = Provide(func) # type: ignore[union-attr]
9
+ return func
10
+
11
+
12
+ def invoke(func: Func) -> Func:
13
+ func._opt = Invoke(func) # type: ignore[union-attr]
14
+ return func
15
+
16
+
17
+ class Block(Iterable[Provide | Invoke]):
18
+ _name: str
19
+ _options: list[Provide | Invoke]
20
+
21
+ def __init__(self, /, block_name: str | None = None) -> None:
22
+ self._options: list[Provide | Invoke] = []
23
+ self._name = block_name or f"{type(self).__name__}"
24
+ for _, method in inspect.getmembers(self):
25
+ if opt := getattr(method, "_opt", None):
26
+ if not isinstance(opt, (Provide, Invoke)):
27
+ raise RuntimeError("Block option is not an instance of Provide or Invoke")
28
+ opt.set_block_name(self._name)
29
+ self._options.append(opt)
30
+
31
+ @property
32
+ def name(self) -> str:
33
+ return self._name
34
+
35
+ def __iter__(self) -> Iterator[Provide | Invoke]:
36
+ return iter(self._options)
engin/_dependency.py ADDED
@@ -0,0 +1,162 @@
1
+ import inspect
2
+ import typing
3
+ from abc import ABC
4
+ from inspect import Parameter, Signature, isclass, iscoroutinefunction
5
+ from typing import (
6
+ Any,
7
+ Awaitable,
8
+ Callable,
9
+ Generic,
10
+ ParamSpec,
11
+ Type,
12
+ TypeAlias,
13
+ TypeVar,
14
+ cast,
15
+ get_type_hints,
16
+ )
17
+
18
+ from engin._type_utils import TypeId, type_id_of
19
+
20
+ P = ParamSpec("P")
21
+ T = TypeVar("T")
22
+ Func: TypeAlias = (
23
+ Callable[P, T] | Callable[P, Awaitable[T]] | Callable[[], T] | Callable[[], Awaitable[T]]
24
+ )
25
+ _SELF = object()
26
+
27
+
28
+ def _noop(*args, **kwargs) -> None: ...
29
+
30
+
31
+ class Dependency(ABC, Generic[P, T]):
32
+ def __init__(self, func: Func[P, T], block_name: str | None = None) -> None:
33
+ self._func = func
34
+ self._is_async = iscoroutinefunction(func)
35
+ self._signature = inspect.signature(self._func)
36
+ self._block_name = block_name
37
+
38
+ @property
39
+ def block_name(self) -> str | None:
40
+ return self._block_name
41
+
42
+ @property
43
+ def name(self) -> str:
44
+ if self._block_name:
45
+ return f"{self._block_name}.{self._func.__name__}"
46
+ else:
47
+ return f"{self._func.__module__}.{self._func.__name__}"
48
+
49
+ @property
50
+ def parameter_types(self) -> list[TypeId]:
51
+ parameters = list(self._signature.parameters.values())
52
+ if not parameters:
53
+ return []
54
+ if parameters[0].name == "self":
55
+ parameters.pop(0)
56
+ return [type_id_of(param.annotation) for param in parameters]
57
+
58
+ @property
59
+ def signature(self) -> Signature:
60
+ return self._signature
61
+
62
+ def set_block_name(self, name: str) -> None:
63
+ self._block_name = name
64
+
65
+ async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
66
+ if self._is_async:
67
+ return await cast(Awaitable[T], self._func(*args, **kwargs))
68
+ else:
69
+ return cast(T, self._func(*args, **kwargs))
70
+
71
+
72
+ class Invoke(Dependency):
73
+ def __init__(self, invocation: Func[P, T], block_name: str | None = None):
74
+ super().__init__(func=invocation, block_name=block_name)
75
+
76
+ def __str__(self) -> str:
77
+ return f"Invoke({self.name})"
78
+
79
+
80
+ class Entrypoint(Invoke):
81
+ def __init__(self, type_: Type[Any], *, block_name: str | None = None) -> None:
82
+ self._type = type_
83
+ super().__init__(invocation=_noop, block_name=block_name)
84
+
85
+ @property
86
+ def parameter_types(self) -> list[TypeId]:
87
+ return [type_id_of(self._type)]
88
+
89
+ @property
90
+ def signature(self) -> Signature:
91
+ return Signature(
92
+ parameters=[
93
+ Parameter(name="x", kind=Parameter.POSITIONAL_ONLY, annotation=self._type)
94
+ ]
95
+ )
96
+
97
+ def __str__(self) -> str:
98
+ return f"Entrypoint({type_id_of(self._type)})"
99
+
100
+
101
+ class Provide(Dependency[Any, T]):
102
+ def __init__(self, builder: Func[P, T], block_name: str | None = None):
103
+ super().__init__(func=builder, block_name=block_name)
104
+ self._is_multi = typing.get_origin(self.return_type) is list
105
+
106
+ if self._is_multi:
107
+ args = typing.get_args(self.return_type)
108
+ if len(args) != 1:
109
+ raise ValueError(
110
+ f"A multiprovider must be of the form list[X], not '{self.return_type}'"
111
+ )
112
+
113
+ @property
114
+ def return_type(self) -> Type[T]:
115
+ if isclass(self._func):
116
+ return_type = self._func # __init__ returns self
117
+ else:
118
+ try:
119
+ return_type = get_type_hints(self._func)["return"]
120
+ except KeyError:
121
+ raise RuntimeError(f"Dependency '{self.name}' requires a return typehint")
122
+
123
+ return return_type
124
+
125
+ @property
126
+ def return_type_id(self) -> TypeId:
127
+ return type_id_of(self.return_type)
128
+
129
+ @property
130
+ def is_multiprovider(self) -> bool:
131
+ return self._is_multi
132
+
133
+ def __hash__(self) -> int:
134
+ return hash(self.return_type_id)
135
+
136
+ def __str__(self) -> str:
137
+ return f"Provide({self.return_type_id})"
138
+
139
+
140
+ class Supply(Provide, Generic[T]):
141
+ def __init__(
142
+ self, value: T, *, type_hint: type | None = None, block_name: str | None = None
143
+ ):
144
+ self._value = value
145
+ self._type_hint = type_hint
146
+ if self._type_hint is not None:
147
+ self._get_val.__annotations__["return"] = type_hint
148
+ super().__init__(builder=self._get_val, block_name=block_name)
149
+
150
+ @property
151
+ def return_type(self) -> Type[T]:
152
+ if self._type_hint is not None:
153
+ return self._type_hint
154
+ if isinstance(self._value, list):
155
+ return list[type(self._value[0])] # type: ignore[misc,return-value]
156
+ return type(self._value)
157
+
158
+ def _get_val(self) -> T:
159
+ return self._value
160
+
161
+ def __str__(self) -> str:
162
+ return f"Supply({self.return_type_id})"
engin/_engin.py ADDED
@@ -0,0 +1,89 @@
1
+ import logging
2
+ from asyncio import Event
3
+ from collections.abc import Iterable
4
+ from itertools import chain
5
+ from typing import ClassVar, TypeAlias
6
+
7
+ from engin import Entrypoint
8
+ from engin._assembler import AssembledDependency, Assembler
9
+ from engin._block import Block
10
+ from engin._dependency import Dependency, Invoke, Provide, Supply
11
+ from engin._lifecycle import Lifecycle
12
+ from engin._type_utils import TypeId
13
+
14
+ LOG = logging.getLogger("engin")
15
+
16
+ Option: TypeAlias = Invoke | Provide | Supply | Block
17
+ _Opt: TypeAlias = Invoke | Provide | Supply
18
+
19
+
20
+ class Engin:
21
+ _LIB_OPTIONS: ClassVar[list[Option]] = [Provide(Lifecycle)]
22
+
23
+ def __init__(self, *options: Option) -> None:
24
+ self._providers: dict[TypeId, Provide] = {}
25
+ self._invokables: list[Invoke] = []
26
+ self._stop_event = Event()
27
+
28
+ self._destruct_options(chain(self._LIB_OPTIONS, options))
29
+ self._assembler = Assembler(self._providers.values())
30
+
31
+ @property
32
+ def assembler(self) -> Assembler:
33
+ return self._assembler
34
+
35
+ async def run(self):
36
+ await self.start()
37
+
38
+ # lifecycle startup
39
+
40
+ # wait till stop signal recieved
41
+ await self._stop_event.wait()
42
+
43
+ # lifecycle shutdown
44
+
45
+ async def start(self) -> None:
46
+ LOG.info("starting engin")
47
+ assembled_invocations: list[AssembledDependency] = [
48
+ await self._assembler.assemble(invocation) for invocation in self._invokables
49
+ ]
50
+ for invocation in assembled_invocations:
51
+ await invocation()
52
+
53
+ lifecycle = await self._assembler.get(Lifecycle)
54
+ await lifecycle.startup()
55
+ self._stop_event = Event()
56
+ LOG.info("startup complete")
57
+
58
+ async def stop(self) -> None:
59
+ self._stop_event.set()
60
+
61
+ def _destruct_options(self, options: Iterable[Option]):
62
+ for opt in options:
63
+ if isinstance(opt, Block):
64
+ self._destruct_options(opt)
65
+ if isinstance(opt, (Provide, Supply)):
66
+ existing = self._providers.get(opt.return_type_id)
67
+ self._log_option(opt, overwrites=existing)
68
+ self._providers[opt.return_type_id] = opt
69
+ elif isinstance(opt, Invoke):
70
+ self._log_option(opt)
71
+ self._invokables.append(opt)
72
+
73
+ @staticmethod
74
+ def _log_option(opt: Dependency, overwrites: Dependency | None = None) -> None:
75
+ if overwrites is not None:
76
+ extra = f"\tOVERWRITES {overwrites.name}"
77
+ if overwrites.block_name:
78
+ extra += f" [{overwrites.block_name}]"
79
+ else:
80
+ extra = ""
81
+ if isinstance(opt, Supply):
82
+ LOG.debug(f"SUPPLY {opt.return_type_id!s:<35}{extra}")
83
+ elif isinstance(opt, Provide):
84
+ LOG.debug(f"PROVIDE {opt.return_type_id!s:<35} <- {opt.name}() {extra}")
85
+ elif isinstance(opt, Entrypoint):
86
+ type_id = opt.parameter_types[0]
87
+ LOG.debug(f"ENTRYPOINT {type_id!s:<35}")
88
+ elif isinstance(opt, Invoke):
89
+ LOG.debug(f"INVOKE {opt.name:<35}")
engin/_exceptions.py ADDED
@@ -0,0 +1,19 @@
1
+ from typing import Any
2
+
3
+ from engin._dependency import Provide
4
+
5
+
6
+ class AssemblyError(Exception):
7
+ def __init__(
8
+ self, provider: Provide[Any], error_type: type[Exception], error_message: str
9
+ ) -> None:
10
+ self.provider = provider
11
+ self.error_type = error_type
12
+ self.error_message = error_message
13
+ self.message = (
14
+ f"provider '{provider.name}' errored with error "
15
+ f"({error_type.__name__}): '{error_message}'"
16
+ )
17
+
18
+ def __str__(self) -> str:
19
+ return self.message
engin/_lifecycle.py ADDED
@@ -0,0 +1,21 @@
1
+ from collections.abc import Callable
2
+ from contextlib import AbstractAsyncContextManager, AsyncExitStack
3
+
4
+
5
+ class Lifecycle:
6
+ def __init__(self) -> None:
7
+ self._on_startup: list[Callable] = []
8
+ self._on_shutdown: list[Callable] = []
9
+ self._context_managers: list[AbstractAsyncContextManager] = []
10
+ self._stack: AsyncExitStack = AsyncExitStack()
11
+
12
+ def register_context(self, cm: AbstractAsyncContextManager):
13
+ self._context_managers.append(cm)
14
+
15
+ async def startup(self) -> None:
16
+ self._stack = AsyncExitStack()
17
+ for cm in self._context_managers:
18
+ await self._stack.enter_async_context(cm)
19
+
20
+ async def shutdown(self) -> None:
21
+ await self._stack.aclose()
engin/_type_utils.py ADDED
@@ -0,0 +1,65 @@
1
+ import typing
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+ _implict_modules = ["builtins", "typing", "collections.abc"]
6
+
7
+
8
+ @dataclass(frozen=True, eq=True)
9
+ class TypeId:
10
+ type: type
11
+ multi: bool
12
+
13
+ @classmethod
14
+ def from_type(cls, type_: Any) -> "TypeId":
15
+ if is_multi_type(type_):
16
+ inner_obj = typing.get_args(type_)[0]
17
+ return TypeId(type=inner_obj, multi=True)
18
+ else:
19
+ return TypeId(type=type_, multi=False)
20
+
21
+ def __str__(self) -> str:
22
+ module = self.type.__module__
23
+ out = f"{module}." if module not in _implict_modules else ""
24
+ out += _args_to_str(self.type)
25
+ if self.multi:
26
+ out += "[]"
27
+ return out
28
+
29
+
30
+ def _args_to_str(type_: Any) -> str:
31
+ args = typing.get_args(type_)
32
+ if args:
33
+ arg_str = f"{type_.__name__}["
34
+ for idx, arg in enumerate(args):
35
+ if isinstance(arg, list):
36
+ arg_str += "["
37
+ for inner_idx, inner_arg in enumerate(arg):
38
+ arg_str += _args_to_str(inner_arg)
39
+ if inner_idx < len(arg) - 1:
40
+ arg_str += ", "
41
+ arg_str += "]"
42
+ elif typing.get_args(arg):
43
+ arg_str += _args_to_str(arg)
44
+ else:
45
+ arg_str += getattr(arg, "__name__", str(arg))
46
+ if idx < len(args) - 1:
47
+ arg_str += ", "
48
+ arg_str += "]"
49
+ else:
50
+ arg_str = type_.__name__
51
+ return arg_str
52
+
53
+
54
+ def type_id_of(type_: Any) -> TypeId:
55
+ """
56
+ Generates a string TypeId for any type.
57
+ """
58
+ return TypeId.from_type(type_)
59
+
60
+
61
+ def is_multi_type(type_: Any) -> bool:
62
+ """
63
+ Discriminates a type to determine whether it is the return type of a multiprovider.
64
+ """
65
+ return typing.get_origin(type_) is list
File without changes
@@ -0,0 +1,55 @@
1
+ import traceback
2
+ import typing
3
+ from typing import Protocol, TypeAlias
4
+
5
+ from engin import Engin, Option
6
+
7
+ __all__ = ["ASGIEngin", "ASGIType"]
8
+
9
+
10
+ _Scope: TypeAlias = typing.MutableMapping[str, typing.Any]
11
+ _Message: TypeAlias = typing.MutableMapping[str, typing.Any]
12
+ _Receive: TypeAlias = typing.Callable[[], typing.Awaitable[_Message]]
13
+ _Send: TypeAlias = typing.Callable[[_Message], typing.Awaitable[None]]
14
+
15
+
16
+ class ASGIType(Protocol):
17
+ async def __call__(self, scope: _Scope, receive: _Receive, send: _Send) -> None: ...
18
+
19
+
20
+ class ASGIEngin(Engin, ASGIType):
21
+ _asgi_app: ASGIType
22
+
23
+ def __init__(self, *options: Option) -> None:
24
+ super().__init__(*options)
25
+
26
+ if not self._assembler.has(ASGIType):
27
+ raise LookupError("A provider for `ASGIType` was expected, none found")
28
+
29
+ async def __call__(self, scope: _Scope, receive: _Receive, send: _Send) -> None:
30
+ if scope["type"] == "lifespan":
31
+ message = await receive()
32
+ receive = _Rereceive(message)
33
+ if message["type"] == "lifespan.startup":
34
+ try:
35
+ await self._startup()
36
+ except Exception as err:
37
+ exc = "".join(traceback.format_exception(err))
38
+ await send({"type": "lifespan.startup.failed", "message": exc})
39
+
40
+ elif message["type"] == "lifespan.shutdown":
41
+ await self.stop()
42
+
43
+ await self._asgi_app(scope, receive, send)
44
+
45
+ async def _startup(self) -> None:
46
+ await self.start()
47
+ self._asgi_app = await self._assembler.get(ASGIType)
48
+
49
+
50
+ class _Rereceive:
51
+ def __init__(self, message: _Message) -> None:
52
+ self._message = message
53
+
54
+ async def __call__(self, *args, **kwargs) -> _Message:
55
+ return self._message
engin/py.typed ADDED
File without changes
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.3
2
+ Name: engin
3
+ Version: 0.0.dev1
4
+ Summary: An async-first modular application framework
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
@@ -0,0 +1,15 @@
1
+ engin/__init__.py,sha256=T2tv-pdULMTp-p0PhB_XYmr9CEWOmMzcu0jebZx7-2Q,475
2
+ engin/_assembler.py,sha256=HPnrTGk6XbO-iG3BZfD65zzOxWC4C1yWOZf7Vv3MMKk,4944
3
+ engin/_block.py,sha256=XrGMFEeps_rvOGsYTlsefUkJGAY6jqdp71G8jE9FbwU,1109
4
+ engin/_dependency.py,sha256=cL1qQFKDt7Rgg7mx8IXVkCZlUigg5cvhPFF9g2LNrgg,4749
5
+ engin/_engin.py,sha256=Sg7sxESuwzTXx-4xgFv869YkpBRxE9d-kjIOTOTtcxU,3071
6
+ engin/_exceptions.py,sha256=nkzTqxrW5nkcNgFDGoZ2TBtnHtO2RLk0qghM5LNAEmU,542
7
+ engin/_lifecycle.py,sha256=0hk24fiwaBos5kaZrnG_Qm0VmUhWKnGtwYCjc007XDk,729
8
+ engin/_type_utils.py,sha256=naEk-lknC3Fdsd4jiP4YZAxjX3KXZN0MhFde9EV-Fmo,1835
9
+ engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ engin/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ engin/extensions/asgi.py,sha256=EHQhevGJt3UJZp_7Y7AIuYVm-bYUO-seuzE3kMuOJWM,1775
12
+ engin-0.0.dev1.dist-info/METADATA,sha256=263kjboSCI7Th0GHl7bbpF5cCHAKsB51_56v5xcJThk,152
13
+ engin-0.0.dev1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
14
+ engin-0.0.dev1.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
15
+ engin-0.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Tim OSullivan
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.