haiway 0.13.1__py3-none-any.whl → 0.14.0__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.
haiway/__init__.py CHANGED
@@ -40,6 +40,7 @@ from haiway.types import (
40
40
  )
41
41
  from haiway.utils import (
42
42
  AsyncQueue,
43
+ AsyncStream,
43
44
  always,
44
45
  as_dict,
45
46
  as_list,
@@ -63,6 +64,7 @@ __all__ = [
63
64
  "MISSING",
64
65
  "ArgumentsTrace",
65
66
  "AsyncQueue",
67
+ "AsyncStream",
66
68
  "AttributePath",
67
69
  "AttributeRequirement",
68
70
  "Default",
haiway/context/access.py CHANGED
@@ -11,7 +11,6 @@ from collections.abc import (
11
11
  Coroutine,
12
12
  Iterable,
13
13
  )
14
- from contextvars import Context, copy_context
15
14
  from logging import Logger
16
15
  from types import TracebackType
17
16
  from typing import Any, final, overload
@@ -24,6 +23,7 @@ from haiway.context.state import StateContext
24
23
  from haiway.context.tasks import TaskGroupContext
25
24
  from haiway.state import State
26
25
  from haiway.utils import mimic_function
26
+ from haiway.utils.stream import AsyncStream
27
27
 
28
28
  __all__ = [
29
29
  "ctx",
@@ -37,7 +37,6 @@ class ScopeContext:
37
37
  "_identifier",
38
38
  "_logger_context",
39
39
  "_metrics_context",
40
- "_state",
41
40
  "_state_context",
42
41
  "_task_group_context",
43
42
  )
@@ -67,13 +66,12 @@ class ScopeContext:
67
66
  )
68
67
  # postponing task group creation to include only when needed
69
68
  self._task_group_context: TaskGroupContext
70
- # postponing state creation to include disposables state when prepared
69
+ # prepare state context to capture current state
71
70
  self._state_context: StateContext
72
- self._state: tuple[State, ...]
73
71
  object.__setattr__(
74
72
  self,
75
- "_state",
76
- state,
73
+ "_state_context",
74
+ StateContext.updated(state),
77
75
  )
78
76
  self._disposables: Disposables | None
79
77
  object.__setattr__(
@@ -115,12 +113,6 @@ class ScopeContext:
115
113
  assert self._disposables is None, "Can't enter synchronous context with disposables" # nosec: B101
116
114
  self._identifier.__enter__()
117
115
  self._logger_context.__enter__()
118
- # lazily initialize state
119
- object.__setattr__(
120
- self,
121
- "_state_context",
122
- StateContext.updated(self._state),
123
- )
124
116
  self._state_context.__enter__()
125
117
  self._metrics_context.__enter__()
126
118
 
@@ -169,24 +161,17 @@ class ScopeContext:
169
161
 
170
162
  # lazily initialize state to include disposables results
171
163
  if self._disposables is not None:
164
+ assert self._state_context._token is None # nosec: B101
172
165
  object.__setattr__(
173
166
  self,
174
167
  "_state_context",
175
- StateContext.updated(
176
- (
177
- *self._state,
178
- *await self._disposables.__aenter__(),
179
- )
168
+ StateContext(
169
+ state=self._state_context._state.updated(
170
+ await self._disposables.__aenter__(),
171
+ ),
180
172
  ),
181
173
  )
182
174
 
183
- else:
184
- object.__setattr__(
185
- self,
186
- "_state_context",
187
- StateContext.updated(self._state),
188
- )
189
-
190
175
  self._state_context.__enter__()
191
176
  self._metrics_context.__enter__()
192
177
 
@@ -401,12 +386,12 @@ class ctx:
401
386
  return TaskGroupContext.run(function, *args, **kwargs)
402
387
 
403
388
  @staticmethod
404
- def stream[Result, **Arguments](
405
- source: Callable[Arguments, AsyncGenerator[Result, None]],
389
+ def stream[Element, **Arguments](
390
+ source: Callable[Arguments, AsyncGenerator[Element, None]],
406
391
  /,
407
392
  *args: Arguments.args,
408
393
  **kwargs: Arguments.kwargs,
409
- ) -> AsyncIterator[Result]:
394
+ ) -> AsyncIterator[Element]:
410
395
  """
411
396
  Stream results produced by a generator within the proper context state.
412
397
 
@@ -427,25 +412,22 @@ class ctx:
427
412
  iterator for accessing generated results
428
413
  """
429
414
 
430
- # prepare context snapshot
431
- context_snapshot: Context = copy_context()
432
-
433
- # prepare nested context
434
- streaming_context: ScopeContext = ctx.scope(
435
- getattr(
436
- source,
437
- "__name__",
438
- "streaming",
439
- )
440
- )
415
+ output_stream = AsyncStream[Element]()
441
416
 
442
- async def generator() -> AsyncGenerator[Result, None]:
443
- async with streaming_context:
417
+ @ctx.scope("stream")
418
+ async def stream() -> None:
419
+ try:
444
420
  async for result in source(*args, **kwargs):
445
- yield result
421
+ await output_stream.send(result)
422
+
423
+ except BaseException as exc:
424
+ output_stream.finish(exception=exc)
425
+
426
+ else:
427
+ output_stream.finish()
446
428
 
447
- # finally return it as an iterator
448
- return context_snapshot.run(generator)
429
+ TaskGroupContext.run(stream)
430
+ return output_stream
449
431
 
450
432
  @staticmethod
451
433
  def check_cancellation() -> None:
@@ -488,7 +470,7 @@ class ctx:
488
470
  StateType
489
471
  resolved state instance
490
472
  """
491
- return StateContext.current(
473
+ return StateContext.state(
492
474
  state,
493
475
  default=default,
494
476
  )
haiway/context/state.py CHANGED
@@ -92,7 +92,7 @@ class StateContext:
92
92
  _context = ContextVar[ScopeState]("StateContext")
93
93
 
94
94
  @classmethod
95
- def current[StateType: State](
95
+ def state[StateType: State](
96
96
  cls,
97
97
  state: type[StateType],
98
98
  /,
haiway/helpers/metrics.py CHANGED
@@ -15,6 +15,7 @@ __all_ = [
15
15
 
16
16
  class MetricsScopeStore:
17
17
  __slots__ = (
18
+ "allow_exit",
18
19
  "entered",
19
20
  "exited",
20
21
  "identifier",
@@ -31,6 +32,7 @@ class MetricsScopeStore:
31
32
  self.entered: float = monotonic()
32
33
  self.metrics: dict[type[State], State] = {}
33
34
  self.exited: float | None = None
35
+ self.allow_exit: bool = False
34
36
  self.nested: list[MetricsScopeStore] = []
35
37
 
36
38
  @property
@@ -115,7 +117,7 @@ class MetricsHolder:
115
117
 
116
118
  def __init__(self) -> None:
117
119
  self.root_scope: ScopeIdentifier | None = None
118
- self.scopes: dict[ScopeIdentifier, MetricsScopeStore] = {}
120
+ self.scopes: dict[str, MetricsScopeStore] = {}
119
121
 
120
122
  def record(
121
123
  self,
@@ -124,10 +126,10 @@ class MetricsHolder:
124
126
  metric: State,
125
127
  ) -> None:
126
128
  assert self.root_scope is not None # nosec: B101
127
- assert scope in self.scopes # nosec: B101
129
+ assert scope.scope_id in self.scopes # nosec: B101
128
130
 
129
131
  metric_type: type[State] = type(metric)
130
- metrics: dict[type[State], State] = self.scopes[scope].metrics
132
+ metrics: dict[type[State], State] = self.scopes[scope.scope_id].metrics
131
133
  if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
132
134
  metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
133
135
 
@@ -142,29 +144,29 @@ class MetricsHolder:
142
144
  merged: bool,
143
145
  ) -> Metric | None:
144
146
  assert self.root_scope is not None # nosec: B101
145
- assert scope in self.scopes # nosec: B101
147
+ assert scope.scope_id in self.scopes # nosec: B101
146
148
 
147
149
  if merged:
148
- return self.scopes[scope].merged(metric)
150
+ return self.scopes[scope.scope_id].merged(metric)
149
151
 
150
152
  else:
151
- return cast(Metric | None, self.scopes[scope].metrics.get(metric))
153
+ return cast(Metric | None, self.scopes[scope.scope_id].metrics.get(metric))
152
154
 
153
155
  def enter_scope[Metric: State](
154
156
  self,
155
157
  scope: ScopeIdentifier,
156
158
  /,
157
159
  ) -> None:
158
- assert scope not in self.scopes # nosec: B101
160
+ assert scope.scope_id not in self.scopes # nosec: B101
159
161
  scope_metrics = MetricsScopeStore(scope)
160
- self.scopes[scope] = scope_metrics
162
+ self.scopes[scope.scope_id] = scope_metrics
161
163
 
162
164
  if self.root_scope is None:
163
165
  self.root_scope = scope
164
166
 
165
167
  else:
166
168
  for key in self.scopes.keys():
167
- if key.scope_id == scope.parent_id:
169
+ if key == scope.parent_id:
168
170
  self.scopes[key].nested.append(scope_metrics)
169
171
  return
170
172
 
@@ -177,8 +179,18 @@ class MetricsHolder:
177
179
  scope: ScopeIdentifier,
178
180
  /,
179
181
  ) -> None:
180
- assert scope in self.scopes # nosec: B101
181
- self.scopes[scope].exited = monotonic()
182
+ assert self.root_scope is not None # nosec: B101
183
+ assert scope.scope_id in self.scopes # nosec: B101
184
+
185
+ self.scopes[scope.scope_id].allow_exit = True
186
+
187
+ if not all(nested.exited for nested in self.scopes[scope.scope_id].nested):
188
+ return # not completed yet
189
+
190
+ self.scopes[scope.scope_id].exited = monotonic()
191
+
192
+ if scope != self.root_scope and self.scopes[scope.parent_id].allow_exit:
193
+ self.exit_scope(self.scopes[scope.parent_id].identifier)
182
194
 
183
195
 
184
196
  @final
@@ -213,7 +225,7 @@ class MetricsLogger:
213
225
  redact_content: bool,
214
226
  ) -> None:
215
227
  self.root_scope: ScopeIdentifier | None = None
216
- self.scopes: dict[ScopeIdentifier, MetricsScopeStore] = {}
228
+ self.scopes: dict[str, MetricsScopeStore] = {}
217
229
  self.items_limit: int | None = items_limit
218
230
  self.redact_content: bool = redact_content
219
231
 
@@ -224,10 +236,10 @@ class MetricsLogger:
224
236
  metric: State,
225
237
  ) -> None:
226
238
  assert self.root_scope is not None # nosec: B101
227
- assert scope in self.scopes # nosec: B101
239
+ assert scope.scope_id in self.scopes # nosec: B101
228
240
 
229
241
  metric_type: type[State] = type(metric)
230
- metrics: dict[type[State], State] = self.scopes[scope].metrics
242
+ metrics: dict[type[State], State] = self.scopes[scope.scope_id].metrics
231
243
  if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
232
244
  metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
233
245
 
@@ -248,29 +260,29 @@ class MetricsLogger:
248
260
  merged: bool,
249
261
  ) -> Metric | None:
250
262
  assert self.root_scope is not None # nosec: B101
251
- assert scope in self.scopes # nosec: B101
263
+ assert scope.scope_id in self.scopes # nosec: B101
252
264
 
253
265
  if merged:
254
- return self.scopes[scope].merged(metric)
266
+ return self.scopes[scope.scope_id].merged(metric)
255
267
 
256
268
  else:
257
- return cast(Metric | None, self.scopes[scope].metrics.get(metric))
269
+ return cast(Metric | None, self.scopes[scope.scope_id].metrics.get(metric))
258
270
 
259
271
  def enter_scope[Metric: State](
260
272
  self,
261
273
  scope: ScopeIdentifier,
262
274
  /,
263
275
  ) -> None:
264
- assert scope not in self.scopes # nosec: B101
276
+ assert scope.scope_id not in self.scopes # nosec: B101
265
277
  scope_metrics = MetricsScopeStore(scope)
266
- self.scopes[scope] = scope_metrics
278
+ self.scopes[scope.scope_id] = scope_metrics
267
279
 
268
280
  if self.root_scope is None:
269
281
  self.root_scope = scope
270
282
 
271
283
  else:
272
284
  for key in self.scopes.keys():
273
- if key.scope_id == scope.parent_id:
285
+ if key == scope.parent_id:
274
286
  self.scopes[key].nested.append(scope_metrics)
275
287
  return
276
288
 
@@ -283,12 +295,22 @@ class MetricsLogger:
283
295
  scope: ScopeIdentifier,
284
296
  /,
285
297
  ) -> None:
286
- assert scope in self.scopes # nosec: B101
287
- self.scopes[scope].exited = monotonic()
298
+ assert self.root_scope is not None # nosec: B101
299
+ assert scope.scope_id in self.scopes # nosec: B101
300
+
301
+ self.scopes[scope.scope_id].allow_exit = True
302
+
303
+ if not all(nested.exited for nested in self.scopes[scope.scope_id].nested):
304
+ return # not completed yet
305
+
306
+ self.scopes[scope.scope_id].exited = monotonic()
307
+
308
+ if scope != self.root_scope and self.scopes[scope.parent_id].allow_exit:
309
+ self.exit_scope(self.scopes[scope.parent_id].identifier)
288
310
 
289
- if scope == self.root_scope and self.scopes[scope].finished:
311
+ elif scope == self.root_scope and self.scopes[self.root_scope.scope_id].finished:
290
312
  if log := _tree_log(
291
- self.scopes[scope],
313
+ self.scopes[scope.scope_id],
292
314
  list_items_limit=self.items_limit,
293
315
  redact_content=self.redact_content,
294
316
  ):
haiway/utils/__init__.py CHANGED
@@ -13,9 +13,11 @@ from haiway.utils.logs import setup_logging
13
13
  from haiway.utils.mimic import mimic_function
14
14
  from haiway.utils.noop import async_noop, noop
15
15
  from haiway.utils.queue import AsyncQueue
16
+ from haiway.utils.stream import AsyncStream
16
17
 
17
18
  __all__ = [
18
19
  "AsyncQueue",
20
+ "AsyncStream",
19
21
  "always",
20
22
  "as_dict",
21
23
  "as_list",
haiway/utils/stream.py ADDED
@@ -0,0 +1,97 @@
1
+ from asyncio import (
2
+ AbstractEventLoop,
3
+ CancelledError,
4
+ Future,
5
+ get_running_loop,
6
+ )
7
+ from collections.abc import AsyncIterator
8
+
9
+ __all__ = [
10
+ "AsyncStream",
11
+ ]
12
+
13
+
14
+ class AsyncStream[Element](AsyncIterator[Element]):
15
+ def __init__(
16
+ self,
17
+ loop: AbstractEventLoop | None = None,
18
+ ) -> None:
19
+ self._loop: AbstractEventLoop = loop or get_running_loop()
20
+ self._ready: Future[None] = self._loop.create_future()
21
+ self._waiting: Future[Element] | None = None
22
+ self._finish_reason: BaseException | None = None
23
+
24
+ @property
25
+ def finished(self) -> bool:
26
+ return self._finish_reason is not None
27
+
28
+ async def send(
29
+ self,
30
+ element: Element,
31
+ /,
32
+ ) -> None:
33
+ if self._finish_reason is not None:
34
+ return # already finished
35
+
36
+ # wait for readiness
37
+ await self._ready
38
+ # we could finish while waiting
39
+ if self._finish_reason is not None:
40
+ return # already finished
41
+
42
+ assert self._waiting is not None and not self._waiting.done() # nosec: B101
43
+ # send the element
44
+ self._waiting.set_result(element)
45
+ # and create new readiness future afterwards
46
+ self._ready = self._loop.create_future()
47
+
48
+ def finish(
49
+ self,
50
+ exception: BaseException | None = None,
51
+ ) -> None:
52
+ if self.finished:
53
+ return # already finished, ignore
54
+
55
+ self._finish_reason = exception or StopAsyncIteration()
56
+
57
+ if not self._ready.done():
58
+ if get_running_loop() is not self._loop:
59
+ self._loop.call_soon_threadsafe(
60
+ self._ready.set_result,
61
+ None,
62
+ )
63
+
64
+ else:
65
+ self._ready.set_result(None)
66
+
67
+ if self._waiting is not None and not self._waiting.done():
68
+ if get_running_loop() is not self._loop:
69
+ self._loop.call_soon_threadsafe(
70
+ self._waiting.set_exception,
71
+ self._finish_reason,
72
+ )
73
+
74
+ else:
75
+ self._waiting.set_exception(self._finish_reason)
76
+
77
+ def cancel(self) -> None:
78
+ self.finish(exception=CancelledError())
79
+
80
+ async def __anext__(self) -> Element:
81
+ assert self._waiting is None, "AsyncStream can't be reused" # nosec: B101
82
+
83
+ if self._finish_reason:
84
+ raise self._finish_reason
85
+
86
+ try:
87
+ assert not self._ready.done() # nosec: B101
88
+ # create new waiting future
89
+ self._waiting = self._loop.create_future()
90
+ # and notify readiness
91
+ self._ready.set_result(None)
92
+ # and wait for the result
93
+ return await self._waiting
94
+
95
+ finally:
96
+ # cleanup waiting future
97
+ self._waiting = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiway
3
- Version: 0.13.1
3
+ Version: 0.14.0
4
4
  Summary: Framework for dependency injection and state management within structured concurrency model.
5
5
  Project-URL: Homepage, https://miquido.com
6
6
  Project-URL: Repository, https://github.com/miquido/haiway.git
@@ -1,18 +1,18 @@
1
- haiway/__init__.py,sha256=ONC4Hk0GaPzhQ3oYmgh6Z4kJdXQiyJ8ZQcM_hCUz-IY,2045
1
+ haiway/__init__.py,sha256=RhW9HOIAVQ3srQ-v23tPghJ20dWcn_uAyt8U8Hhn868,2081
2
2
  haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  haiway/context/__init__.py,sha256=feqd0eJnGQwh4B8BZXpS0fQRE-DqoFCFOHipF1jOY8A,762
4
- haiway/context/access.py,sha256=CXGe-qkKeG5352_Bo7D4UusnEEQ_SYmRG94RyK5fG8Q,18951
4
+ haiway/context/access.py,sha256=1Oq70pcp54bC9NNp-zMocDewVJpcZgepi99_xeqz98o,18502
5
5
  haiway/context/disposables.py,sha256=vcsh8jRaJ8Q1ob7oh5LsrSPw9f5AMTcaD_p_Gb7tXAI,2588
6
6
  haiway/context/identifier.py,sha256=lz-FuspOtsaEsfb7QPrEVWYfbcMJgd3A6BGG3kLbaV0,3914
7
7
  haiway/context/logging.py,sha256=F3dr6MLjodg3MX5WTInxn3r3JuihG-giBzumI0GGUQw,5590
8
8
  haiway/context/metrics.py,sha256=N20XQtC8au_e_3iWrsZdej78YBEIWF44fdtWcZBWono,5223
9
- haiway/context/state.py,sha256=qskYoNwN5Ad0OgnyhL-PyGzTZltwVVdE9CEqWWn4lm8,4554
9
+ haiway/context/state.py,sha256=7pXb5gvyPOWiRbxX-sSfO-hjaHcTUIp_uTKhjaSLeRo,4552
10
10
  haiway/context/tasks.py,sha256=MKfsa-921cIpQ_BKskwokjR27suCHkHZa3O9kOE8UOg,2826
11
11
  haiway/context/types.py,sha256=VvJA7wAPZ3ISpgyThVguioYUXqhHf0XkPfRd0M1ERiQ,142
12
12
  haiway/helpers/__init__.py,sha256=ZKDlL3twDqXyI1a9FDgRy3m1-Dfycvke6BJ4C3CndEk,671
13
13
  haiway/helpers/asynchrony.py,sha256=YHLK5Hjc-5UWlQRypC11yHeEQyeAtHqrMoBTBfqQBvQ,6286
14
14
  haiway/helpers/caching.py,sha256=EU5usTHGDzf0SO3bMW4hHB9oZlLlE7BxO_2ckbjYBw8,13274
15
- haiway/helpers/metrics.py,sha256=lCSvat3IrkmytFdqTvsqkVqYcVOK_bByfwYAe0hJIWg,13614
15
+ haiway/helpers/metrics.py,sha256=VNxgPgV8pgt-51f2CANy1IVx8VMYIAxT3F849t3IeQs,14604
16
16
  haiway/helpers/retries.py,sha256=3m1SsJW_YY_HPufX9LEzcd_MEyRRFNXvSExLeEti8W8,7539
17
17
  haiway/helpers/throttling.py,sha256=r9HnUuo4nX36Pf-oMFHUJk-ZCDeXQ__JTDHlkSltRhA,4121
18
18
  haiway/helpers/timeouted.py,sha256=DthIm4ytKhmiIKf-pcO_vrO1X-ImZh-sLNCWcLY9gfw,3337
@@ -27,7 +27,7 @@ haiway/types/__init__.py,sha256=-j4uDN6ix3GBXLBqXC-k_QOJSDlO6zvNCxDej8vVzek,342
27
27
  haiway/types/default.py,sha256=h38-zFkbn_UPEiw1SdDF5rkObVmD9UJpmyhOgS1gQ9U,2208
28
28
  haiway/types/frozen.py,sha256=CZhFCXnWAKEhuWSfILxA8smfdpMd5Ku694ycfLh98R8,76
29
29
  haiway/types/missing.py,sha256=rDnyA2wxPkTbJl0L-zbo0owp7IJ04xkCIp6xD6wh8NI,1712
30
- haiway/utils/__init__.py,sha256=YBq9hYhrHFB-4d_M53A620-2KEz5SMU31GDBW6gXFnQ,804
30
+ haiway/utils/__init__.py,sha256=JYo5EVquL2BCBsHtvySPTio_x5hSVDJCfu_naWzbqKE,867
31
31
  haiway/utils/always.py,sha256=u1tssiErzm0Q3ASc3CV1rLhcMQ54MjpMlC_bRJMQhK4,1230
32
32
  haiway/utils/collections.py,sha256=IzD-XSEyngKyzLTNG9sr7QjXIneoAzi3oRsDmbRHtzU,3276
33
33
  haiway/utils/env.py,sha256=-hI4CgLkzdyueuECVjm-TfR3lQjE2bDsc72w7vNC4nQ,5339
@@ -36,7 +36,8 @@ haiway/utils/logs.py,sha256=oDsc1ZdqKDjlTlctLbDcp9iX98Acr-1tdw-Pyg3DElo,1577
36
36
  haiway/utils/mimic.py,sha256=BkVjTVP2TxxC8GChPGyDV6UXVwJmiRiSWeOYZNZFHxs,1828
37
37
  haiway/utils/noop.py,sha256=qgbZlOKWY6_23Zs43OLukK2HagIQKRyR04zrFVm5rWI,344
38
38
  haiway/utils/queue.py,sha256=Tk1bXvuNbEgapeC3-h_PYBASqVjhEoL8mUvtJnM29xI,4000
39
- haiway-0.13.1.dist-info/METADATA,sha256=9YaEi2sxzl_QL76iqwkhmKrWJh_0EQsoaucu9Yf0eXo,4299
40
- haiway-0.13.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
41
- haiway-0.13.1.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
42
- haiway-0.13.1.dist-info/RECORD,,
39
+ haiway/utils/stream.py,sha256=Vqyi0EwcupkVyKQ7eple6z9DkcbSHkE-6yMw85mak9Q,2832
40
+ haiway-0.14.0.dist-info/METADATA,sha256=e01xN8K8-d8RiPRaV2ibtsxEHfITllUZmxZA_VzEpBs,4299
41
+ haiway-0.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
42
+ haiway-0.14.0.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
43
+ haiway-0.14.0.dist-info/RECORD,,