engin 0.1.0rc1__py3-none-any.whl → 0.1.0rc2__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/_cli/_graph.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import contextlib
2
+ import json
2
3
  import socketserver
3
4
  import threading
4
5
  from http.server import BaseHTTPRequestHandler
@@ -9,9 +10,10 @@ from typing import Annotated, Any
9
10
  import typer
10
11
  from rich import print
11
12
 
12
- from engin import Entrypoint, Invoke, TypeId
13
+ from engin import Engin, Entrypoint, Invoke, TypeId
13
14
  from engin._cli._common import COMMON_HELP, get_engin_instance
14
15
  from engin._dependency import Dependency, Provide, Supply
16
+ from engin._graph import Node
15
17
  from engin.extensions.asgi import ASGIEngin
16
18
 
17
19
  try:
@@ -46,37 +48,11 @@ def serve_graph(
46
48
 
47
49
  nodes = instance.graph()
48
50
 
49
- # transform dependencies into mermaid syntax
50
- dependencies = [
51
- f"{_render_node(node.parent)} --> {_render_node(node.node)}"
52
- for node in nodes
53
- if node.parent is not None
54
- and not (node.node.block_name and node.node.block_name == node.parent.block_name)
55
- ]
51
+ # Generate JSON data for interactive graph
52
+ graph_data = _generate_graph_data(nodes, instance)
56
53
 
57
- blocks = {node.node.block_name for node in nodes if node.node.block_name is not None}
58
-
59
- # group blocks into subgraphs
60
- for block in blocks:
61
- dependencies.append(f"subgraph {block}")
62
- dependencies.extend(
63
- [
64
- f"{_render_node(node.parent, False)} --> {_render_node(node.node, False)}"
65
- for node in nodes
66
- if node.parent is not None
67
- and node.node.block_name == block
68
- and node.parent.block_name == block
69
- ]
70
- )
71
- dependencies.append("end")
72
-
73
- html = (
74
- _GRAPH_HTML.replace("%%DATA%%", "\n".join(dependencies))
75
- .replace(
76
- "%%LEGEND%%",
77
- ASGI_ENGIN_LEGEND if isinstance(instance, ASGIEngin) else DEFAULT_LEGEND,
78
- )
79
- .encode("utf8")
54
+ html = _GRAPH_HTML.replace("%%GRAPH_DATA%%", json.dumps(graph_data, indent=2)).encode(
55
+ "utf8"
80
56
  )
81
57
 
82
58
  class Handler(BaseHTTPRequestHandler):
@@ -108,47 +84,130 @@ def wait_for_interrupt() -> None:
108
84
  sleep(10000)
109
85
 
110
86
 
111
- _BLOCK_IDX: dict[str, int] = {}
112
- _SEEN_BLOCKS: list[str] = []
87
+ def _generate_graph_data(nodes: list[Node], instance: Engin) -> dict[str, Any]:
88
+ """Generate JSON data structure for interactive graph rendering."""
89
+ all_deps = set()
90
+ for node in nodes:
91
+ all_deps.add(node.node)
92
+ if node.parent:
93
+ all_deps.add(node.parent)
94
+
95
+ # Generate node data
96
+ node_data = []
97
+ for dep in all_deps:
98
+ node_info = _get_node_info(dep)
99
+ node_data.append(node_info)
100
+
101
+ # Generate edge data
102
+ edge_data = [
103
+ {
104
+ "from": f"n{id(node.parent)}",
105
+ "to": f"n{id(node.node)}",
106
+ "from_block": node.parent.block_name,
107
+ "to_block": node.node.block_name,
108
+ }
109
+ for node in nodes
110
+ if node.parent is not None
111
+ ]
113
112
 
113
+ # Get block information
114
+ blocks = list({node.node.block_name for node in nodes if node.node.block_name is not None})
114
115
 
115
- def _render_node(node: Dependency, render_block: bool = True) -> str:
116
- node_id = id(node)
117
- md = ""
118
- style = ""
116
+ # Generate legend
117
+ legend = ASGI_ENGIN_LEGEND if isinstance(instance, ASGIEngin) else DEFAULT_LEGEND
119
118
 
120
- # format block name
121
- if render_block and (n := node.block_name):
122
- md += f"_{n}_\n"
119
+ return {
120
+ "nodes": node_data,
121
+ "edges": edge_data,
122
+ "blocks": blocks,
123
+ "legend": legend,
124
+ "app_origin": _APP_ORIGIN,
125
+ }
123
126
 
124
- node_root_package = node.source_package.split(".", maxsplit=1)[0]
125
- if node_root_package != _APP_ORIGIN:
126
- if style:
127
- style += "E"
128
- else:
129
- style = "external"
130
127
 
131
- if style:
132
- style = f":::{style}"
128
+ def _get_node_info(node: Dependency) -> dict[str, Any]:
129
+ """Extract node information for JSON representation."""
130
+ node_id = f"n{id(node)}" # Add 'n' prefix to match mermaid node IDs
131
+ label = ""
132
+ style_classes = []
133
133
 
134
+ # Determine if external
135
+ node_root_package = node.source_package.split(".", maxsplit=1)[0]
136
+ is_external = node_root_package != _APP_ORIGIN
137
+ if is_external:
138
+ style_classes.append("external")
139
+
140
+ # Collect detailed information for tooltips
141
+ details: dict[str, Any] = {
142
+ "full_name": node.name,
143
+ "source_module": node.source_module,
144
+ "source_package": node.source_package,
145
+ "parameters": [],
146
+ "return_type": None,
147
+ "scope": None,
148
+ }
149
+
150
+ # Get parameter information
151
+ if hasattr(node, "parameter_type_ids"):
152
+ details["parameters"] = [str(param_id) for param_id in node.parameter_type_ids]
153
+
154
+ # Determine node type and extract specific details
134
155
  if isinstance(node, Supply):
135
- md += f"{_short_name(node.return_type_id)}"
136
- return f'{node_id}("`{md}`"){style}'
137
- if isinstance(node, Provide):
138
- md += f"{_short_name(node.return_type_id)}"
139
- return f'{node_id}["`{md}`"]{style}'
140
- if isinstance(node, Entrypoint):
156
+ node_type = "Supply"
157
+ label += f"{_short_name(node.return_type_id)}"
158
+ shape = "round"
159
+ details["return_type"] = str(node.return_type_id)
160
+ if hasattr(node, "_value"):
161
+ details["value_type"] = type(node._value).__name__
162
+ elif isinstance(node, Provide):
163
+ node_type = "Provide"
164
+ label += f"{_short_name(node.return_type_id)}"
165
+ shape = "rect"
166
+ details["return_type"] = str(node.return_type_id)
167
+ details["factory_function"] = node.func_name
168
+ if node.scope:
169
+ details["scope"] = node.scope
170
+ style_classes.append(f"scope-{node.scope}")
171
+ if node.is_multiprovider:
172
+ details["multiprovider"] = True
173
+ style_classes.append("multi")
174
+ elif isinstance(node, Entrypoint):
175
+ node_type = "Entrypoint"
141
176
  entrypoint_type = node.parameter_type_ids[0]
142
- md += f"{entrypoint_type}"
143
- return f'{node_id}[/"`{md}`"\\]{style}'
144
- if isinstance(node, Invoke):
145
- md += f"{node.func_name}"
146
- return f'{node_id}[/"`{md}`"/]{style}'
147
- if isinstance(node, APIRouteDependency):
148
- md += f"{node.name}"
149
- return f'{node_id}[["`{md}`"]]{style}'
177
+ label += f"{entrypoint_type}"
178
+ shape = "trapezoid"
179
+ details["entrypoint_type"] = str(entrypoint_type)
180
+ elif isinstance(node, Invoke):
181
+ node_type = "Invoke"
182
+ label += f"{node.func_name}"
183
+ shape = "trapezoid"
184
+ details["function"] = node.func_name
185
+ elif APIRouteDependency is not None and isinstance(node, APIRouteDependency):
186
+ node_type = "APIRoute"
187
+ label += f"{node.name}"
188
+ shape = "subroutine"
189
+ if hasattr(node, "route"):
190
+ details["methods"] = (
191
+ list(node.route.methods) if hasattr(node.route, "methods") else []
192
+ )
193
+ details["path"] = getattr(node.route, "path", "")
150
194
  else:
151
- return f'{node_id}["`{node.name}`"]{style}'
195
+ node_type = "Other"
196
+ label += f"{node.name}"
197
+ shape = "rect"
198
+
199
+ return {
200
+ "id": node_id,
201
+ "label": label,
202
+ "type": node_type,
203
+ "external": is_external,
204
+ "block": node.block_name,
205
+ "shape": shape,
206
+ "style_classes": style_classes,
207
+ "source_module": node.source_module,
208
+ "source_package": node.source_package,
209
+ "details": details,
210
+ }
152
211
 
153
212
 
154
213
  def _short_name(name: TypeId) -> str:
engin/_cli/_inspect.py CHANGED
@@ -11,7 +11,7 @@ from engin._cli._common import COMMON_HELP, get_engin_instance, print_error
11
11
  cli = typer.Typer()
12
12
  _CLI_HELP = {
13
13
  "type": "Filter providers by the provided type, e.g. `AsyncClient` or `float[]`",
14
- "module": "Filter providers by the provided types' module, e.g. `engin` or `httpx`",
14
+ "module": "Filter providers by the provided type's module, e.g. `engin` or `httpx`",
15
15
  "verbose": "Enables verbose output",
16
16
  }
17
17
 
@@ -74,8 +74,11 @@ def serve_graph(
74
74
  available = sorted(map(str, instance.assembler.providers))
75
75
  print_error(f"No matching providers, available: {available}")
76
76
 
77
- if matching_provider_count > 1:
78
- console.print(f"Found {matching_provider_count} matching providers", style="dim")
77
+ console.print(
78
+ f"Found {matching_provider_count} matching provider"
79
+ + ("s" if matching_provider_count > 1 else ""),
80
+ style="dim",
81
+ )
79
82
 
80
83
  table = Table(show_header=False, show_lines=False, box=box.ASCII)
81
84
 
engin/_engin.py CHANGED
@@ -61,8 +61,8 @@ class Engin:
61
61
  the Invoke options parameters are assembled.
62
62
  2. All Invocations are run sequentially in the order they were passed in to the Engin.
63
63
  3. Lifecycle Startup tasks registered by assembled dependencies are run sequentially.
64
- 4. The Engin waits for a stop signal, i.e. SIGINT or SIGTERM, or for something to
65
- set the ShutdownSwitch event.
64
+ 4. The Engin waits for a stop signal, i.e. SIGINT or SIGTERM, or a supervised task that
65
+ causes a shutdown.
66
66
  5. Lifecyce Shutdown tasks are run in the reverse order to the Startup order.
67
67
 
68
68
  Examples:
engin/_supervisor.py CHANGED
@@ -58,19 +58,33 @@ class _SupervisorTask:
58
58
  await self.factory()
59
59
  self.complete = True
60
60
  return
61
- except get_cancelled_exc_class():
62
- LOG.info(f"{self.name} cancelled")
61
+ except get_cancelled_exc_class() as err:
62
+ LOG.debug(f"supervised task '{self.name}' was cancelled", exc_info=err)
63
63
  raise
64
64
  except Exception as err:
65
- LOG.error(f"Supervisor: {self.name} raised {type(err).__name__}", exc_info=err)
66
65
  self.last_exception = err
67
66
 
68
67
  if self.on_exception == OnException.IGNORE:
68
+ LOG.warning(
69
+ f"supervisor task '{self.name}' raised {type(err).__name__} "
70
+ "which will be ignored",
71
+ exc_info=err,
72
+ )
69
73
  self.complete = True
70
74
  return
71
75
  if self.on_exception == OnException.RETRY:
76
+ LOG.warning(
77
+ f"supervisor task '{self.name}' raised {type(err).__name__} "
78
+ "which will be retried",
79
+ exc_info=err,
80
+ )
72
81
  continue
73
82
  if self.on_exception == OnException.SHUTDOWN:
83
+ LOG.error(
84
+ f"supervisor task '{self.name}' raised {type(err).__name__}, "
85
+ "starting shutdown",
86
+ exc_info=err,
87
+ )
74
88
  self.complete = True
75
89
  raise get_cancelled_exc_class() from None
76
90
  assert_never(self.on_exception)
engin/exceptions.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from typing import TYPE_CHECKING, Any
2
2
 
3
3
  from engin._dependency import Provide
4
+ from engin._type_utils import TypeId
4
5
 
5
6
  if TYPE_CHECKING:
6
7
  from engin._block import Block
@@ -12,12 +13,6 @@ class EnginError(Exception):
12
13
  """
13
14
 
14
15
 
15
- class AssemblerError(EnginError):
16
- """
17
- Base class for all custom exceptions raised by the Assembler.
18
- """
19
-
20
-
21
16
  class InvalidBlockError(EnginError):
22
17
  """
23
18
  Raised when an invalid block is instantiated.
@@ -32,6 +27,25 @@ class InvalidBlockError(EnginError):
32
27
  return self.message
33
28
 
34
29
 
30
+ class AssemblerError(EnginError):
31
+ """
32
+ Base class for all custom exceptions raised by the Assembler.
33
+ """
34
+
35
+
36
+ class TypeNotProvidedError(AssemblerError):
37
+ """
38
+ Raised when the Assembler cannot assemble a type due to a missing Provider.
39
+ """
40
+
41
+ def __init__(self, type_id: TypeId) -> None:
42
+ self.type_id = type_id
43
+ self.message = f"no provider found for '{type_id}'"
44
+
45
+ def __str__(self) -> str:
46
+ return self.message
47
+
48
+
35
49
  class ProviderError(AssemblerError):
36
50
  """
37
51
  Raised when a Provider errors during Assembly.
@@ -77,4 +91,5 @@ __all__ = [
77
91
  "InvalidBlockError",
78
92
  "NotInScopeError",
79
93
  "ProviderError",
94
+ "TypeNotProvidedError",
80
95
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.1.0rc1
3
+ Version: 0.1.0rc2
4
4
  Summary: An async-first modular application framework
5
5
  Project-URL: Homepage, https://github.com/invokermain/engin
6
6
  Project-URL: Documentation, https://engin.readthedocs.io/en/latest/
@@ -33,13 +33,13 @@ Engin is a lightweight application framework powered by dependency injection, it
33
33
  you build and maintain large monoliths and many microservices.
34
34
 
35
35
 
36
- ## Features
36
+ ## Feature
37
37
 
38
- The Engin framework includes:
38
+ The Engin framework gives you:
39
39
 
40
40
  - A fully-featured dependency injection system.
41
41
  - A robust application runtime with lifecycle hooks and supervised background tasks.
42
- - Zero boiler-plate code reuse across multiple applications.
42
+ - Zero boiler-plate code reuse across applications.
43
43
  - Integrations for other frameworks such as FastAPI.
44
44
  - Full async support.
45
45
  - CLI commands to aid local development.
@@ -47,7 +47,7 @@ The Engin framework includes:
47
47
 
48
48
  ## Installation
49
49
 
50
- Engin is available on PyPI, install using your favourite dependency manager:
50
+ Engin is available on PyPI, install it using your favourite dependency manager:
51
51
 
52
52
  - `pip install engin`
53
53
  - `poetry add engin`
@@ -55,13 +55,13 @@ Engin is available on PyPI, install using your favourite dependency manager:
55
55
 
56
56
  ## Example
57
57
 
58
- A small example which shows some of the runtime features of Engin. This application
59
- makes a http request and then performs a shutdown.
58
+ A small example which shows some of the features of Engin. This application
59
+ makes 3 http requests and shuts itself down.
60
60
 
61
61
  ```python
62
62
  import asyncio
63
63
  from httpx import AsyncClient
64
- from engin import Engin, Invoke, Lifecycle, Provide, Supervisor
64
+ from engin import Engin, Invoke, Lifecycle, OnException, Provide, Supervisor
65
65
 
66
66
 
67
67
  def httpx_client_factory(lifecycle: Lifecycle) -> AsyncClient:
@@ -76,13 +76,17 @@ async def main(
76
76
  httpx_client: AsyncClient,
77
77
  supervisor: Supervisor,
78
78
  ) -> None:
79
- async def http_request():
80
- await httpx_client.get("https://httpbin.org/get")
81
- # one we've made the http request shutdown the application
82
- raise asyncio.CancelledError("Forcing shutdown")
79
+ async def http_requests_task():
80
+ # simulate a background task
81
+ for x in range(3):
82
+ await httpx_client.get("https://httpbin.org/get")
83
+ await asyncio.sleep(1.0)
84
+ # raise an error to shutdown the application, normally you wouldn't do this!
85
+ raise RuntimeError("Forcing shutdown")
86
+
87
+ # supervise the http requests as part of the application's lifecycle
88
+ supervisor.supervise(http_requests_task, on_exception=OnException.SHUTDOWN)
83
89
 
84
- # supervise the http request as part of the application's lifecycle
85
- supervisor.supervise(http_request)
86
90
 
87
91
  # define our modular application
88
92
  engin = Engin(Provide(httpx_client_factory), Invoke(main))
@@ -97,6 +101,15 @@ With logs enabled this will output:
97
101
  INFO:engin:starting engin
98
102
  INFO:engin:startup complete
99
103
  INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
104
+ INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
105
+ INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
106
+ ERROR:engin:supervisor task 'http_requests_task' raised RuntimeError, starting shutdown
107
+ Traceback (most recent call last):
108
+ File "C:\dev\python\engin\src\engin\_supervisor.py", line 58, in __call__
109
+ await self.factory()
110
+ File "C:\dev\python\engin\readme_example.py", line 29, in http_requests_task
111
+ raise RuntimeError("Forcing shutdown")
112
+ RuntimeError: Forcing shutdown
100
113
  INFO:engin:stopping engin
101
114
  INFO:engin:shutdown complete
102
115
  ```
@@ -106,4 +119,4 @@ INFO:engin:shutdown complete
106
119
  Engin is heavily inspired by [Uber's Fx framework for Go](https://github.com/uber-go/fx)
107
120
  and the [Injector framework for Python](https://github.com/python-injector/injector).
108
121
 
109
- They are both great projects, check them out.
122
+ They are both great projects, go check them out.
@@ -1,27 +1,27 @@
1
1
  engin/__init__.py,sha256=O0vS570kZFBq7Kwy4FgeJFIhfo4aIg5mv_Z_9vAQRio,577
2
- engin/_assembler.py,sha256=MC14BRsgabGlq9weyv2VXylH4RE282uNTyNH5rN8Lqc,11359
2
+ engin/_assembler.py,sha256=0uXgtcO5M3EHg0I-TQK9y7LOzfxkLFmKia-zLyVHaxA,11178
3
3
  engin/_block.py,sha256=IacP4PoJKRhSQCbQSdoyCtmu362a4vj6qoUQKyaJwzI,3062
4
4
  engin/_dependency.py,sha256=xINk3sudxzsTmkUkNAKQwzBc0G0DfhpnrZli4z3ALBY,9459
5
- engin/_engin.py,sha256=Eui-CtEjlF-PaXzZXZjUdB6ByTHJJan4hFMhPDLLJuE,9537
5
+ engin/_engin.py,sha256=oGaf_iedMNKxl3rbADpPzIvNtTx1Pfs-6o0e8yRrmHk,9532
6
6
  engin/_graph.py,sha256=y1g7Lm_Zy5GPEgRsggCKV5DDaDzcwUl8v3IZCK8jyGI,1631
7
7
  engin/_introspect.py,sha256=VdREX6Lhhga5SnEP9G7mjHkgJR4mpqk_SMnmL2zTcqY,966
8
8
  engin/_lifecycle.py,sha256=cSWe3euZkmpxmUPFvph2lsTtvuZbxttEfBL-RnOI7lo,5325
9
9
  engin/_option.py,sha256=nZcdrehp1QwgxMUoIpsM0PJuu1q1pbXzhcVsetbsHpc,223
10
- engin/_supervisor.py,sha256=37h036bPe7ew88WjpIOmhZwCvOdLVcvalyJgbWZr1vU,3716
10
+ engin/_supervisor.py,sha256=HI0D4StqSJZE2l6x7RtouRLKWx1HOhUmLHqu8pUcWbQ,4343
11
11
  engin/_type_utils.py,sha256=H3Tl-kJr2wY2RhaTXP9GrMqa2RsXMijHbjHKe1AxGmc,2276
12
- engin/exceptions.py,sha256=-VPwPReZb9YEIkrWMR9TW2K5HEwmHHgEO7QWH6wfV8c,1946
12
+ engin/exceptions.py,sha256=lSMOJI4Yl-VIM0yDzFWbPhC0mQm4f0WvGULr9SldIaY,2353
13
13
  engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  engin/_cli/__init__.py,sha256=Ixk3NoZeIN8Bj53I625uqJdLyyT9Gpbe_4GtNy-KQwM,636
15
- engin/_cli/_check.py,sha256=FCGFKs5kD3ImicNDz2B4aOEOliwqfoa7uAjKoZRQpHo,2274
15
+ engin/_cli/_check.py,sha256=YA37Gi4rimKIH-XMs7SEAFkSRNBIMG8OCKfF3W1V3-g,1976
16
16
  engin/_cli/_common.py,sha256=6tyjxAkROCViw0LOFdx-X1U-iSXKyeW5CoE9UxWRybI,3282
17
- engin/_cli/_graph.html,sha256=rR5dnDKoz7KtSff0ERCi2UKuoH_Z03MRYiXI_W03G5k,2430
18
- engin/_cli/_graph.py,sha256=jvk_CPe47z2nF4yTo9T4BTAyFdy2nIsjjcZTrG2kRf0,4714
19
- engin/_cli/_inspect.py,sha256=0ok7yglRHF29S31K9HChnWpsuxp13cB8g1Ret1hwJrM,3122
17
+ engin/_cli/_graph.html,sha256=YIv34LR00aWsWgjNrqO4XBNu4frPo_y-i1CijaZyySo,29073
18
+ engin/_cli/_graph.py,sha256=MsxsNpL1v0v1AUT57ZS97l1diwacqRaPdVBObHHIGJE,6753
19
+ engin/_cli/_inspect.py,sha256=_uzldpHA51IX4srpUGzL4lZNiepqucsO9M3Zo83XBBM,3159
20
20
  engin/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  engin/extensions/asgi.py,sha256=7vQFaVs1jxq1KbhHGN8k7x2UFab6SPUq2_hXfX6HiXU,3329
22
22
  engin/extensions/fastapi.py,sha256=7N6i-eZUEZRPo7kcvjS7kbRSY5QAPyKJXSeongSQ-OA,6371
23
- engin-0.1.0rc1.dist-info/METADATA,sha256=oKmRR7rU91tH7IQIZqi9gMUrdYIBi1ghO_O4R8Gz8qQ,3274
24
- engin-0.1.0rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- engin-0.1.0rc1.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
26
- engin-0.1.0rc1.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
27
- engin-0.1.0rc1.dist-info/RECORD,,
23
+ engin-0.1.0rc2.dist-info/METADATA,sha256=I7BtKglKAs30NQ6n73p3gzLhZhxcvsIZXuuM356Ipa4,3951
24
+ engin-0.1.0rc2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ engin-0.1.0rc2.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
26
+ engin-0.1.0rc2.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
27
+ engin-0.1.0rc2.dist-info/RECORD,,